This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

View | Details | Raw Unified | Return to bug 29466
Collapse All | Expand All

(-)openide/openide-spec-vers.properties (-2 / +1 lines)
Line 7 Link Here
7
org.openide.specification.version=4.10
7
org.openide.specification.version=4.11
8
--
(-)openide/api/doc/changes/apichanges.xml (-2 / +27 lines)
Line 116 Link Here
116
116
    <change id="Utilities.renderHTML">
117
--
117
     <api name="nodes"/>
118
     <summary>Lightweight HTML rendering methods</summary>
119
     <version major="4" minor="11"/>
120
     <date day="26" month="8" year="2003"/>
121
     <author login="tboudreau"/>
122
     <compatibility addition="yes" deprecation="no" />
123
     <description>
124
     A lightweight HTML renderer which can render a limited subset of
125
     HTML has been added to the APIs, and will be used in Explorer.
126
     Nodes wishing to provide text rendered in HTML may do so by
127
     returning subset-compliant, HTML formatted text from the new
128
     method <code>getFormattedDisplayName</code>.  An interface,
129
     <code>HTMLStatus</code> has been created which extends 
130
     <code>FileSystem.Status</code>, has been created, which allows
131
     filesystems to supply HTML formatted status information, by
132
     implementing it on their <code>FileSystem.Status</code> implementation.
133
     If one is present, DataNode will use it to supply HTML formatted
134
     text to Explorer.
135
     </description>
136
     <class package="org.openide.util" name="Utilities"/>
137
     <class package="org.openide.filesystems.FileSystem" name="HTMLStatus"/>
138
     <class package="org.openide.nodes" name="Node"/>
139
     <class package="org.openide.loaders" name="DataNode"/>
140
     <issue number="29466"/>
141
    </change>
142
    
(-)openide/loaders/src/org/openide/loaders/DataNode.java (+29 lines)
Line 155 Link Here
155
    
156
    /** Get a display name formatted using the limited HTML subset supported
157
     * by <code>Utilities.renderString()</code>.  If the underlying 
158
     * <code>FileSystem.Status</code> is an instance of HTMLStatus,
159
     * this method will return non-null if status information is added.
160
     *
161
     * @return a string containing compliant HTML markup or null
162
     * @see org.openide.util.Utilities.renderHTML
163
     * @see org.openide.nodes.Node.getFormattedDisplayName
164
     * @since 1.73 */
165
    public String getFormattedDisplayName() {
166
        try {
167
            FileSystem.Status stat = 
168
                obj.getPrimaryFile().getFileSystem().getStatus();
169
            if (stat instanceof FileSystem.HTMLStatus) {
170
                FileSystem.HTMLStatus hstat = (FileSystem.HTMLStatus) stat;
171
                String result = hstat.annotateNameHTML (
172
                    super.getDisplayName(), obj.files());
173
                
174
                //Make sure the super string was really modified
175
                if (!super.getDisplayName().equals(result)) {
176
                    return result;
177
                }
178
            }
179
        } catch (FileStateInvalidException e) {
180
            //do nothing and fall through
181
        }
182
        return super.getFormattedDisplayName();
183
    }
(-)openide/src/org/openide/explorer/view/NodeRenderer.java (-25 / +204 lines)
Line 218 Link Here
218
218
    
219
--
219
    static class BaseRenderer extends javax.swing.JComponent {
220
        private javax.swing.Icon icon=null;
221
        private String txt="";//NOI18N
222
        private int iconTextGap=0;
223
        protected boolean hasFocus=false;
224
        protected boolean selected = false;
225
        private boolean ndCalcPrefSize=true;
226
        private Color selectionForeground=null;
227
        private Color selectionBackground=null;
228
        private Color selectionBorder=null;
229
        
230
        private boolean isHTML=true;
231
        
232
        public BaseRenderer() {
233
            updateUI();
234
        }
235
        
236
        public void setHTML(boolean html) {
237
            isHTML = html;
238
        }
239
        
240
        public javax.swing.Icon getIcon() {
241
            return icon;
242
        }
243
        java.awt.Dimension prefSize = null;
244
        
245
        public java.awt.Dimension getPreferredSize () {
246
            if (ndCalcPrefSize) {
247
                calcPrefSize();
248
            }
249
            return prefSize;
250
        }
251
        
252
        public void updateUI() {
253
            super.updateUI();
254
            selectionForeground = 
255
                UIManager.getColor ("Tree.selectionForeground"); //NOI18N
256
            selectionBackground =
257
                UIManager.getColor ("Tree.selectionBackground"); //NOI18N
258
            selectionBorder =
259
                UIManager.getColor ("Tree.selectionBorderColor"); //NOI18N
260
            if (selectionForeground == null) {
261
                selectionForeground = Color.BLACK;
262
            }
263
            if (selectionBackground == null) {
264
                selectionBackground = new Color (153,153,204);
265
            }
266
            if (selectionBorder == null) {
267
                selectionBorder = new Color (102, 102, 153);
268
            }
269
        }
270
        
271
        private void calcPrefSize() {
272
            if (prefSize == null) {
273
                prefSize = new java.awt.Dimension();
274
            }
275
            java.awt.Font f = getFont();
276
            java.awt.Graphics g = getGraphics();
277
            if ((f == null) || (g == null)) {
278
                //We're just initializing the component, supply some dummy 
279
                //values and quit
280
                prefSize.width = 30;
281
                prefSize.height = 16;
282
                return;
283
            }
284
            java.awt.FontMetrics fm = g.getFontMetrics(f);
285
            
286
            if (icon == null) {
287
                prefSize.height = fm.getHeight();
288
            } else {
289
                prefSize.height = Math.max (fm.getHeight(), icon.getIconHeight());
290
            }
291
            int w;
292
            if ((txt == null) || (txt.length()==0)) {
293
                prefSize.width = icon != null ? icon.getIconWidth() : 0;
294
            } else {
295
                if (isHTML) {
296
                    prefSize.width = Math.round(Math.round(
297
                    Utilities.renderString(txt, g, 0, 0, Integer.MAX_VALUE, 
298
                    Integer.MAX_VALUE, f, Color.BLACK, Utilities.STYLE_CLIP, 
299
                    false))) + 1;
300
                } else {
301
                    prefSize.width = Math.round(Math.round(
302
                    Utilities.renderPlainString(txt, g, 0, 0, Integer.MAX_VALUE, 
303
                    Integer.MAX_VALUE, f, Color.BLACK, Utilities.STYLE_CLIP, 
304
                    false))) + 1;
305
                }
306
            }
307
            if (icon != null) {
308
                prefSize.width += icon.getIconWidth() + iconTextGap;
309
            }
310
        }
311
        
312
        public void setIconTextGap (int val) {
313
            iconTextGap = val;
314
        }
315
        
316
        public int getIconTextGap () {
317
            return iconTextGap;
318
        }
319
        
320
        public void setIcon(javax.swing.Icon i) {
321
            if (i != icon) {
322
                icon = i;
323
                ndCalcPrefSize=true;
324
            }
325
        }
326
        
327
        public String getText() {
328
            return txt;
329
        }
330
        
331
        public void setText(String s) {
332
            if (s != txt) {
333
                txt = s;
334
                ndCalcPrefSize = true;
335
            }
336
        }
337
        
338
        public void paint (java.awt.Graphics g) {
339
            java.awt.Point p = getLocation();
340
            
341
            int w = icon == null ? 0 : icon.getIconWidth();
342
            int h = icon == null ? 0 : icon.getIconHeight();
343
            
344
            int width = getWidth();
345
            int height = getHeight();
346
            
347
            g.setColor (selected ? selectionBackground : getBackground()); //XXX
348
            int rectStart = w + iconTextGap-1;
349
            if (selected) {
350
                g.fillRect (rectStart, 0, getPreferredSize().width - (rectStart+1), height);
351
            }
352
            if (hasFocus) {
353
                g.setColor (selectionBorder); //XXX
354
                g.drawRect(rectStart, 0, getPreferredSize().width-(rectStart+1), height-1);
355
            }
356
            
357
            if (icon != null) {
358
                int iconY = 0;
359
                if (height > h) {
360
                    iconY = (height - h) / 2;
361
                }
362
                icon.paintIcon(this, g, 0, iconY);
363
            }
364
            java.awt.FontMetrics fm = g.getFontMetrics(getFont());
365
            int baseline = fm.getHeight() - fm.getDescent();
366
            
367
            int stringX = icon == null ? 0 : icon.getIconWidth() 
368
                + iconTextGap;
369
            if (g.hitClip (stringX, 0, width, height)) {
370
                if (isHTML) {
371
                Utilities.renderHTML (txt, g, 
372
                    stringX, 
373
                    baseline,
374
                    Integer.MAX_VALUE, 
375
                    Integer.MAX_VALUE, getFont(), 
376
                    selected ? selectionForeground : getForeground(), 
377
                    Utilities.STYLE_CLIP, 
378
                    true);
379
                } else {
380
                Utilities.renderString (txt, g, 
381
                    stringX, 
382
                    baseline,
383
                    Integer.MAX_VALUE, 
384
                    Integer.MAX_VALUE, getFont(), 
385
                    selected ? selectionForeground : getForeground(), 
386
                    Utilities.STYLE_CLIP, 
387
                    true);
388
                }
389
            }
390
        }
391
    }
Line 221 Link Here
221
    final static class Tree extends DefaultTreeCellRenderer {
394
    static class Tree extends BaseRenderer implements TreeCellRenderer {
222
--
Line 255 Link Here
255
            setText(vis.getDisplayName ());
428
            String s = vis.getFormattedDisplayName();
256
--
429
            if (s == null) {
430
                s = vis.getDisplayName();
431
                setHTML(false);
432
            } else {
433
                setHTML(true);
434
            }
435
            setText(s);
Line 262 Link Here
262
	    this.hasFocus = hasFocus;
442
            this.hasFocus = hasFocus;
263
--
Lines 265-269 Link Here
265
            if(sel) {
445
            setForeground (tree.getForeground()); 
266
                setForeground(getTextSelectionColor());
267
            } else {
268
                setForeground(getTextNonSelectionColor());
269
            }
270
--
Line 287 Link Here
287
    static final class List extends JLabel implements ListCellRenderer {
463
    static final class List extends BaseRenderer implements ListCellRenderer {
288
--
Line 313 Link Here
313
            setText(vis.getDisplayName ());
489
            String s = vis.getFormattedDisplayName();
314
--
490
            if (s == null) {
491
                s = vis.getDisplayName();
492
                setHTML(false);
493
            } else {
494
                setHTML(true);
495
            }
496
            setText(s);
Line 349 Link Here
349
    final static class Pane extends JLabel implements ListCellRenderer {
532
    final static class Pane extends BaseRenderer implements ListCellRenderer {
350
--
Lines 359-361 Link Here
359
            setVerticalTextPosition(JLabel.BOTTOM);
360
            setHorizontalAlignment(JLabel.CENTER);
361
            setHorizontalTextPosition(JLabel.CENTER);
Line 380 Link Here
380
            setText(vis.getDisplayName ());
560
            
381
--
561
            String s = vis.getFormattedDisplayName();
562
            if (s == null) {
563
                s = vis.getDisplayName();
564
                setHTML(false);
565
            } else {
566
                setHTML(true);
567
            }
568
            setText(s);
(-)openide/src/org/openide/explorer/view/TreeTable.java (-9 / +18 lines)
Line 66 Link Here
66
        NodeRenderer rend = NodeRenderer.sharedInstance ();
66
        NodeRenderer.Tree rend = new TTRenderer();//NodeRenderer.sharedInstance ();
67
--
Line 110 Link Here
110
    /** Renderer subclass which hacks the clip rectangle.  This should be
111
     *  set from the renderer's getPreferredSize method (this works correctly
112
     *  for a standard JTree but doesn't work for the embedded tree) */
113
    private class TTRenderer extends NodeRenderer.Tree {
114
        public void paint (Graphics g) {
115
            //hack the clipping rectangle
116
            Rectangle r = g.getClipBounds();
117
            r.width = TreeTable.this.getWidth();
118
            g.setClip(r.x, r.y, r.width, r.height);
119
            super.paint (g);
120
        }
121
    }
122
    
Line 384 Link Here
384
        
397
    
385
--
Lines 826-827 Link Here
826
		    this.clearSelection ();
839
                    this.clearSelection ();
827
                    if(min != -1 && max != -1) {
840
		    if(min != -1 && max != -1) {
828
--
Line 833 Link Here
833
				if(selPath != null) {
846
                                if(selPath != null) {
834
--
(-)openide/src/org/openide/explorer/view/TreeViewCellEditor.java (-17 / +17 lines)
Line 44 Link Here
44
44
    protected NodeRenderer.Tree bren;
45
--
Lines 49-50 Link Here
49
    public TreeViewCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
49
    public TreeViewCellEditor(JTree tree, NodeRenderer.Tree bren) { //XXX , TreeCellRenderer renderer) {
50
        super(tree, renderer);
50
        super(tree,new DefaultTreeCellRenderer()); 
51
--
51
        this.bren = bren;
Lines 201-209 Link Here
201
	if(renderer != null) {
202
        if(bren != null) {
202
	    renderer.getTreeCellRendererComponent(tree, value, sel, expanded,
203
            bren.getTreeCellRendererComponent(tree, value, sel, expanded,
203
			    leaf, row, true);
204
                    leaf, row, true);
204
	    editingIcon = renderer.getIcon ();
205
            editingIcon = bren.getIcon();
205
            offset = renderer.getIconTextGap () + editingIcon.getIconWidth ();
206
            offset = bren.getIconTextGap () + editingIcon.getIconWidth ();
206
	} else {
207
        } else {
207
	    editingIcon = null;
208
            editingIcon = null;
208
	    offset = 0;
209
            offset = 0;
209
	}								      
210
        }								      
210
--
Line 267 Link Here
268
Line 268 Link Here
268
    static class Ed extends DefaultCellEditor {
270
    class Ed extends DefaultCellEditor {
269
--
Line 283 Link Here
285
            
Line 289 Link Here
292
(-)openide/src/org/openide/explorer/view/VisualizerNode.java (-2 / +11 lines)
Line 97 Link Here
97
    /** cached formated display name */
98
    private String formattedDisplayName;
Line 145 Link Here
145
            displayName = node == null ? null : node.getDisplayName ();
147
            displayName = node == null ? null : node.getDisplayName();
146
--
Line 150 Link Here
152
    public String getFormattedDisplayName () {
153
        if (formattedDisplayName == UNKNOWN) {
154
            displayName = node == null ? null : node.getFormattedDisplayName();
155
        }
156
        return formattedDisplayName;
157
    }
158
    
Line 334 Link Here
343
        formattedDisplayName = node.getFormattedDisplayName ();
(-)openide/src/org/openide/filesystems/FileSystem.java (+15 lines)
Line 689 Link Here
689
    /** An extension to the Status interface to allow filesystems to provide
690
     *  HTML markup in a status string for display components.
691
     *  @since 1.74
692
     */
693
    public static interface HTMLStatus extends Status {
694
        /** Provide status annotation including HTML markup, using
695
	 * the limited subset of HTML markup supported by
696
         * <code>Utilities.renderString()</code>.  The returned markup should
697
         * contain opening and closing &lt;HTML&gt; tags
698
         * @since 1.74
699
         * @see org.openide.util.Utilities.renderHTML
700
	 */
701
        public String annotateNameHTML (String name, java.util.Set files);
702
    }
703
(-)openide/src/org/openide/nodes/FilterNode.java (+16 lines)
Line 400 Link Here
400
    
401
    /** Get the formatted display name for the node.  FilterNode
402
     * subclasses which do not delegate the display name must
403
     * override this method to return a formatted display name.
404
     *
405
     * @see org.openide.nodes.Node.getFormattedDisplayName
406
     * @return the formatted display name of the original node if
407
     *  delegating the display name to the original node, or null
408
     */
409
    public String getFormattedDisplayName() {
410
        if (delegating (DELEGATE_GET_DISPLAY_NAME)) {
411
            return original.getFormattedDisplayName();
412
        } else {
413
            return null;
414
        }
415
    }
(-)openide/src/org/openide/nodes/Node.java (+32 lines)
Line 97 Link Here
97
    /** Property for a node's formatted display name.  Clients interested in
98
     * this property should also assume it has changed if they receive an event
99
     * of <code>PROP_DISPLAY_NAME</code>. */
100
    public static final String PROP_FORMATTED_DISPLAY_NAME = 
101
        "formattedDisplayName"; //NOI18N
102
    
Line 309 Link Here
315
    }
316
    
317
    /** Get a display name containing inline markup, using the limited
318
     * subset of HTML supported by <code>Utilities.renderString()</code>.
319
     * Explorer views will render nodes which return non-null from
320
     * this method using its return value rather than the result of 
321
     * <code>getDisplayName()</code>.  Other uses of a Node's display
322
     * name (such as logging code) will use <code>getDisplayName()</code>.
323
     * <P>
324
     * Nodes that do not support HTML-ized display names should return
325
     * null.  Note that, unlike with Swing components, the String returned
326
     * by this method need not contain opening HTML tags.<P>
327
     * The default implementation returns null.
328
     * <P>
329
     * Nodes may fire formatting-only changes by firing 
330
     * <code>PROP_FORMATTED_DISPLAY_NAME</code>.
331
     * <P>
332
     * Implementations whose display name may contain &gt; or &lt; characters
333
     * should take care to escape these characters or return null from this
334
     * method.
335
     * @return a string containing HTML compliant with the limited subset
336
     * of HTML supported by the lightweight renderer.
337
     * @see org.openide.util.Utilities.renderHTML
338
     * @since 1.73 */
339
    public String getFormattedDisplayName() {
340
        return null;
(-)openide/src/org/openide/util/Utilities.java (+872 lines)
Line 21 Link Here
21
import java.awt.font.LineMetrics;
22
import java.awt.geom.Rectangle2D;
Line 35 Link Here
37
import java.util.Stack;
Line 39 Link Here
42
import javax.swing.UIManager;
Line 2511 Link Here
2515
    }
2516
    
2517
2518
    /** Constant used by <code>renderString</code>, <code>renderPlainString</code>
2519
     * and <code>renderHTML</code> if painting should simply be cut off at
2520
     * the boundary of the cooordinates passed.     */
2521
    public static final int STYLE_CLIP=0;
2522
    /** Constant used by <code>renderString</code>, <code>renderPlainString</code>
2523
     * and <code>renderHTML</code> if painting should produce an ellipsis (...)
2524
     * if the text would overlap the boundary of the coordinates passed */
2525
    public static final int STYLE_TRUNCATE=1;
2526
    //make public if at some point we want to support word-wrap (nd to implement
2527
    //for renderPlainString as well)
2528
    /** Constant used by <code>renderString</code>, <code>renderPlainString</code>
2529
     * and <code>renderHTML</code> if painting should word wrap the text.  In
2530
     * this case, the return value of any of the above methods will be the
2531
     * height, rather than width painted. */
2532
    private static final int STYLE_WORDWRAP=2; 
2533
    /**Render a string to a graphics canvas, using the same API as renderHTML().
2534
     * Can render a string using JLabel-style ellipsis (...) in the case that
2535
     * it will not fit in the passed rectangle, if the style parameter is
2536
     * STYLE_CLIP. Returns the width in pixels successfully painted.
2537
     * <strong>This method is not thread-safe and should not be called off
2538
     * the AWT thread!</strong>
2539
     *
2540
     * @see org.openide.util.Utilities.renderHTML */
2541
    public static double renderPlainString (String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint) {
2542
    //assert SwingUtilities.isEventDispatchThread(); //XXX once build supports this, uncomment
2543
        //per Jarda's request, keep the word wrapping code but don't expose it.                                               
2544
        if (style < 0 || style > 1) {
2545
            throw new IllegalArgumentException (
2546
                "Unknown rendering mode: " + style); //NOI18N
2547
        }
2548
        return _renderPlainString (s, g, x, y, w, h, f, defaultColor, style, 
2549
            paint);
2550
    }
2551
    
2552
    
2553
    private static double _renderPlainString (String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint) {
2554
        g.setColor (defaultColor);
2555
        g.setFont (f);
2556
        FontMetrics fm = g.getFontMetrics(f);
2557
        Rectangle2D r = fm.getStringBounds(s, g);
2558
        if ((r.getWidth() <= w) || (style == STYLE_CLIP)) {
2559
            if (paint) {
2560
                g.drawString(s, x, y);
2561
            }
2562
        } else {
2563
            char[] chars = new char[s.length()];
2564
            s.getChars(0, s.length()-1, chars, 0);
2565
            if (chars.length == 0) {
2566
                return 0;
2567
            }
2568
            double chWidth = r.getWidth() / chars.length;
2569
            int estCharsOver = new Double((r.getWidth() - w) / chWidth).intValue();
2570
            if (style == STYLE_TRUNCATE) {
2571
                int length = chars.length - estCharsOver;
2572
                if (length <=0) {
2573
                    return 0;
2574
                }
2575
                if (paint) {
2576
                    if (length > 3) {
2577
                        Arrays.fill (chars, length-3, length, '.');
2578
                        g.drawChars(chars, 0, length, x, y);
2579
                    } else {
2580
                        g.drawString("...", x,y);
2581
                    }
2582
                }
2583
            } else {
2584
                //XXX implement plaintext word wrap if we want to support it at some point
2585
            }
2586
        }
2587
        return r.getWidth();
2588
    }
2589
    
2590
    
2591
    /** Render a string to a graphics context, using HTML markup if the string
2592
     * begins with html tags.  Delegates to <code>renderPlainString()</code>
2593
     * or <code>renderHTML()</code> as appropriate.  See the documentation for
2594
     * <code>renderHTML()</code> for details of the subset of HTML that is
2595
     * supported. 
2596
     * <P><strong>This method is not thread-safe and should not be called off
2597
     * the AWT thread.</strong>
2598
     * @param s The string to render
2599
     * @param g A graphics object into which the string should be drawn, or which should be
2600
     * used for calculating the appropriate size
2601
     * @param x The x coordinate to paint at.
2602
     * @param y The y position at which to paint.  Note that this method does not calculate font
2603
     * height/descent - this value should be the baseline for the line of text, not
2604
     * the upper corner of the rectangle to paint in.
2605
     * @param w The maximum width within which to paint.
2606
     * @param h The maximum height within which to paint.
2607
     * @param f The base font to be used for painting or calculating string width/height.
2608
     * @param defaultColor The base color to use if no font color is specified as html tags
2609
     * @param style The wrapping style to use, either <code>STYLE_CLIP</CODE>,
2610
     * or <CODE>STYLE_TRUNCATE</CODE>
2611
     * @param paint True if actual painting should occur.  If false, this method will not actually
2612
     * paint anything, only return a value representing the width/height needed to
2613
     * paint the passed string.
2614
     * @return The width in pixels required
2615
     * to paint the complete string, or the passed parameter <code>w</code> if it is
2616
     * smaller than the required width.
2617
     */
2618
    public static double renderString (String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint) {
2619
        if (s.startsWith("<html") || s.startsWith("<HTML")) { //NOI18N
2620
            return renderHTML (s, g, x, y, w, h, f, defaultColor, style, paint);
2621
        } else {
2622
            return renderPlainString (s, g, x, y, w, h, f, defaultColor, style, paint);
2623
        }
2624
    }    
2625
2626
    /** Render a string as HTML using a fast, lightweight renderer supporting a limited
2627
     * subset of HTML.  The following tags are supported, in upper or lower case:
2628
     *
2629
     * <table>
2630
     * <tr>
2631
     *  <td>&lt;B&gt;</td>
2632
     *  <td>Boldface text</td>
2633
     * </tr>
2634
     * <tr>
2635
     *  <td>&lt;S&gt;</td>
2636
     *  <td>Strikethrough text</td>
2637
     * </tr>
2638
     * <tr>
2639
     *  <td>&lt;U&gt;</td>
2640
     *  <td>Underline text</td>
2641
     * </tr>
2642
     * <tr>
2643
     *  <td>&lt;I&gt;</td>
2644
     *  <td>Italic text</td>
2645
     * </tr>
2646
     * <tr>
2647
     *  <td>&lt;EM&gt;</td>
2648
     *  <td>Emphasized text (same as italic)</td>
2649
     * </tr>
2650
     * <tr>
2651
     *  <td>&lt;STRONG&gt;</td>
2652
     *  <td>Strong text (same as bold)</td>
2653
     * </tr>
2654
     * <tr>
2655
     *  <td>&lt;font&gt;</td>
2656
     *  <td>Font color - font attributes other than color are not supported.  Colors
2657
     *  may be specified as hexidecimal strings, such as #FF0000 or as logical colors
2658
     *  defined in the current look and feel by specifying a ! character as the first
2659
     *  character of the color name.  Logical colors are colors available from the
2660
     *  current look and feel's UIManager.  For example, <code>&lt;font
2661
     *  color=&quot;!Tree.background&quot;&gt;</code> will set the font color to the
2662
     *  result of <code>UIManager.getColor(&quot;Tree.background&quot)</code>.
2663
     * <strong>Font size tags are not supported.</strong>
2664
     * </td>
2665
     * </tr>
2666
     * </table>
2667
     * The lightweight html renderer supports the following named sgml character
2668
     * entities: <code>quot, lt, amp, lsquo, rsquo, ldquo, rdquo, ndash, mdash, ne,
2669
     * le, ge, copy, reg, trade.  </code>.  It also supports numeric entities
2670
     * (e.g. <code>&amp;8822;</code>).
2671
     * <p><b>When to use this method instead of the JDK's HTML support: </b> when
2672
     * rendering short strings (for example, in a tree or table cell renderer)
2673
     * with limited HTML, this method is approximately 10x faster than JDK HTML
2674
     * rendering (it does not build and parse a document tree).
2675
     *
2676
     * <P><B><U>Specifying logical colors</U></B><BR>
2677
     * Hardcoded text colors are undesirable, as they can be incompatible (even 
2678
     * invisible) on some look and feels or themes.  
2679
     * The lightweight HTML renderer supports a non-standard syntax for specifying
2680
     * font colors via a key for a color in the UI defaults for the current look
2681
     * and feel.  This is accomplished by prefixing the key name with a <code>!</code>
2682
     * character.  For example: <code>&lt;font color='!controlShadow'&gt;</code>.
2683
     *
2684
     * <P><B><U>Modes of operation</U></B><BR>
2685
     * This method supports two modes of operation:
2686
     * <OL>
2687
     * <LI><CODE>STYLE_CLIP</CODE> - as much text as will fit in the pixel width passed
2688
     * to the method should be painted, and the text should be cut off at the maximum
2689
     * width or clip rectangle maximum X boundary for the graphics object, whichever is
2690
     * smaller.</LI>
2691
     * <LI><CODE>STYLE_TRUNCATE</CODE> - paint as much text as will fit in the pixel
2692
     * width passed to the method, but paint the last three characters as .'s, in the
2693
     * same manner as a JLabel truncates its text when the available space is too
2694
     * small.</LI>
2695
     * </OL>
2696
     * <P>
2697
     * This method can also be used in non-painting mode to establish the space
2698
     * necessary to paint a string.  This is accomplished by passing the value of the
2699
     * <code>paint</code> argument as false.  The return value will be the required
2700
     * width in pixels
2701
     * to display the text.  Note that in order to retrieve an
2702
     * accurate value, the argument for available width should be passed
2703
     * as <code>Integer.MAX_VALUE</code> or an appropriate maximum size - otherwise
2704
     * the return value will either be the passed maximum width or the required
2705
     * width, whichever is smaller.  Also, the clip shape for the passed graphics
2706
     * object should be null or a value larger than the maximum possible render size.
2707
     * <P>
2708
     * This method will log a warning if it encounters HTML markup it cannot
2709
     * render.  To aid diagnostics, if NetBeans is run with the argument 
2710
     * <code>-J-Dnetbeans.lwhtml.strict=true</code> an exception will be thrown
2711
     * when an attempt is made to render unsupported HTML.</code><p>
2712
     * <strong>This method is not thread-safe and should not be called off
2713
     * the AWT thread!</strong>
2714
     * <p>
2715
     * @param s The string to render
2716
     * @param g A graphics object into which the string should be drawn, or which should be
2717
     * used for calculating the appropriate size
2718
     * @param x The x coordinate to paint at.
2719
     * @param y The y position at which to paint.  Note that this method does not calculate font
2720
     * height/descent - this value should be the baseline for the line of text, not
2721
     * the upper corner of the rectangle to paint in.
2722
     * @param w The maximum width within which to paint.
2723
     * @param h The maximum height within which to paint.
2724
     * @param f The base font to be used for painting or calculating string width/height.
2725
     * @param defaultColor The base color to use if no font color is specified as html tags
2726
     * @param style The wrapping style to use, either <code>STYLE_CLIP</CODE>,
2727
     * or <CODE>STYLE_TRUNCATE</CODE>
2728
     * @param paint True if actual painting should occur.  If false, this method will not actually
2729
     * paint anything, only return a value representing the width/height needed to
2730
     * paint the passed string.
2731
     * @return The width in pixels required
2732
     * to paint the complete string, or the passed parameter <code>w</code> if it is
2733
     * smaller than the required width.
2734
     */    
2735
    public static double renderHTML (String s, Graphics g, int x, int y, 
2736
                                           int w, int h, Font f, 
2737
                                           Color defaultColor, int style, 
2738
                                           boolean paint) {
2739
    //assert SwingUtilities.isEventDispatchThread(); //XXX once build supports this, uncomment
2740
        
2741
        //per Jarda's request, keep the word wrapping code but don't expose it.                                               
2742
        if (style < 0 || style > 1) {
2743
            throw new IllegalArgumentException (
2744
                "Unknown rendering mode: " + style); //NOI18N
2745
        }
2746
        return _renderHTML (s, g, x, y, w, h, f, defaultColor, style, 
2747
            paint);
2748
    }
2749
    
2750
    /** Stack object used during HTML rendering to hold previous colors in
2751
     * the case of nested color entries. */
2752
    private static Stack colorStack = null; //XXX check synchronization overhead, maybe find an unsynchronized stack impl?
2753
    
2754
        /** Implementation of HTML rendering */
2755
    private static double _renderHTML (String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint) {
2756
        g.setColor (defaultColor);
2757
        g.setFont (f);
2758
        char[] chars = s.toCharArray();
2759
        int pos = 0; //skip the opening <html> tag
2760
        int origX = x;
2761
        boolean done = false;  //flag if rendering completed, either by finishing the string or running out of space
2762
        boolean inTag = false; //flag if the current position is inside a tag, and the tag should be processed rather than rendering
2763
        boolean inClosingTag = false; //flag if the current position is inside a closing tag
2764
        boolean strikethrough = false; //flag if a strikethrough line should be painted
2765
        boolean underline = false; //flag if an underline should be painted
2766
        boolean bold = false; //flag if text is currently bold
2767
        boolean italic = false; //flag if text is currently italic
2768
        boolean truncated = false; //flag if the last possible character has been painted, and the next loop should paint "..." and return
2769
        double widthPainted = 0; //the total width painted, for calculating needed space
2770
        double heightPainted = 0; //the total height painted, for calculating needed space
2771
        boolean lastWasWhitespace = false; //flag to skip additional whitespace if one whitespace char already painted
2772
        double lastHeight=0; //the last line height, for calculating total required height
2773
        
2774
        /* How this all works, for anyone maintaining this code (hopefully it will
2775
          never need it):
2776
          1. The string is converted to a char array
2777
          2. Loop over the characters.  Variable pos is the current point.
2778
            2a. See if we're in a tag by or'ing inTag with currChar == '<'
2779
              If WE ARE IN A TAG:
2780
               2a1: is it an opening tag?
2781
                 If YES:
2782
                   - Identify the tag, Configure the Graphics object with
2783
                     the appropriate font, color, etc.  Set pos = the first
2784
                     character after the tag
2785
                 If NO (it's a closing tag)
2786
                   - Identify the tag.  Reconfigure the Graphics object
2787
                     with the state it should be in outside the tag
2788
                     (reset the font if italic, pop a color off the stack, etc.)
2789
            If WE ARE NOT IN A TAG
2790
               - Locate the next < or & character or the end of the string
2791
               - Paint the characters using the Graphics object
2792
               - Check underline and strikethrough tags, and paint line if
2793
                 needed
2794
            See if we're out of space, and do the right thing for the style
2795
            (paint ..., give up or skip to the next line)
2796
         */
2797
2798
        if (colorStack == null) {
2799
            //create the stack used for storing colors in nested color tags
2800
            colorStack = new Stack();
2801
        } else {
2802
            //clear it in case some bad html left junk behind
2803
            colorStack.clear();
2804
        }
2805
        
2806
        //Enter the painting loop
2807
        while (!done) {
2808
            if (pos == s.length()) {
2809
                return widthPainted;
2810
            }
2811
            //see if we're in a tag
2812
            try {
2813
                inTag |= chars[pos] == '<';
2814
            } catch (ArrayIndexOutOfBoundsException e) {
2815
                //Should there be any problem, give a meaningful enough
2816
                //message to reproduce the problem
2817
                ArrayIndexOutOfBoundsException aib = 
2818
                    new ArrayIndexOutOfBoundsException(
2819
                    "HTML rendering failed at position " + pos + " in String \""
2820
                    + s + "\".  Please report this at http://www.netbeans.org"); //NOI18N
2821
                throw aib;
2822
            }
2823
            inClosingTag = inTag && (pos+1 < chars.length) && chars[pos+1] 
2824
                == '/'; //NOI18N
2825
            
2826
            if (truncated) {
2827
                //Then we've almost run out of space, time to print ... and quit
2828
                g.setColor (defaultColor);
2829
                g.setFont (f);
2830
                if (paint) {
2831
                    g.drawString("...", x, y); //NOI18N
2832
                }
2833
                done = true;
2834
            } else if (inTag) {
2835
                //If we're in a tag, don't paint, process it
2836
                pos++;
2837
                int tagEnd = pos;
2838
                while (!done && (chars[tagEnd] != '>')) {
2839
                    done = tagEnd == chars.length -1;
2840
                    tagEnd++;
2841
                }
2842
                
2843
                if (inClosingTag) {
2844
                    //Handle closing tags by resetting the Graphics object (font, etc.)
2845
                    pos++;
2846
                    switch (chars[pos]) {
2847
                        case 'P' :
2848
                        case 'p' :
2849
                        case 'H' : 
2850
                        case 'h' : break; //ignore html opening/closing tags
2851
                        case 'B' :
2852
                        case 'b' : 
2853
                           if (chars[pos+1] == 'r' || chars[pos+1] == 'R') {
2854
                               break;
2855
                           }
2856
                           if (!bold) {
2857
                              throwBadHTML ("Closing bold tag w/o " + //NOI18N
2858
                                "opening bold tag", pos, chars); //NOI18N
2859
                           }
2860
                           if (italic) {
2861
                              g.setFont (f.deriveFont (Font.ITALIC));
2862
                           } else {
2863
                              g.setFont (f.deriveFont (Font.PLAIN));
2864
                           }
2865
                           bold = false;
2866
                           break;
2867
                        case 'E' :
2868
                        case 'e' : //em tag
2869
                        case 'I' :
2870
                        case 'i' : 
2871
                           if (bold) {
2872
                              g.setFont (f.deriveFont (Font.BOLD));
2873
                           } else {
2874
                              g.setFont (f.deriveFont (Font.PLAIN));
2875
                           }
2876
                           if (!italic) {
2877
                               throwBadHTML ("Closing italics tag w/o" //NOI18N
2878
                                   + "opening italics tag", pos, chars); //NOI18N
2879
                           }
2880
                           italic = false;
2881
                           break;
2882
                        case 'S' :
2883
                        case 's' : 
2884
                           switch (chars[pos+1]) {
2885
                           case 'T' :
2886
                           case 't' : if (italic) {
2887
                                         g.setFont (f.deriveFont (
2888
                                            Font.ITALIC));
2889
                                      } else {
2890
                                         g.setFont (f.deriveFont (
2891
                                            Font.PLAIN));
2892
                                      }
2893
                                      bold = false;                                       
2894
                                      break;
2895
                           case '>' :
2896
                                strikethrough = false;
2897
                                break;
2898
                           }
2899
                           break;
2900
                        case 'U' :
2901
                        case 'u' : underline = false;
2902
                                   break;
2903
                        case 'F' :
2904
                        case 'f' : 
2905
                           if (colorStack.isEmpty()) {
2906
                               g.setColor (defaultColor);
2907
                           } else {
2908
                                g.setColor ((Color) colorStack.pop());
2909
                           }
2910
                           break;
2911
                        default  : 
2912
                            throwBadHTML (
2913
                                "Malformed or unsupported HTML", //NOI18N
2914
                                pos,  chars);
2915
                    }
2916
                } else {
2917
                    //Okay, we're in an opening tag.  See which one and configure the Graphics object
2918
                    switch (chars[pos]) {
2919
                        case 'B' :
2920
                        case 'b' : 
2921
                           switch (chars[pos+1]) {
2922
                               case 'R' :
2923
                               case 'r' :
2924
                                         if (style == STYLE_WORDWRAP) {
2925
                                            x = origX;
2926
                                            int lineHeight = g.getFontMetrics().getHeight();
2927
                                            y += lineHeight;
2928
                                            heightPainted += lineHeight;
2929
                                            widthPainted = 0;
2930
                                         }
2931
                                         break;
2932
                               case '>' :
2933
                                   bold = true;
2934
                                   if (italic) {
2935
                                       g.setFont (f.deriveFont (Font.BOLD | Font.ITALIC));
2936
                                   } else {
2937
                                       g.setFont (f.deriveFont (Font.BOLD));
2938
                                   }
2939
                                   break;
2940
                              }
2941
                              break;
2942
                        case 'e' : //em tag
2943
                        case 'E' :
2944
                        case 'I' :
2945
                        case 'i' : 
2946
                           italic = true;
2947
                           if (bold) {
2948
                               g.setFont (f.deriveFont (Font.ITALIC | Font.BOLD));
2949
                           } else {
2950
                               g.setFont (f.deriveFont (Font.ITALIC));
2951
                           }
2952
                           break;
2953
                        case 'S' :
2954
                        case 's' : 
2955
                          switch (chars[pos+1]) {
2956
                              case '>' :
2957
                                strikethrough = true;
2958
                                break;
2959
                              case 'T' :
2960
                              case 't' :
2961
                                   bold = true;
2962
                                   if (italic) {
2963
                                       g.setFont (f.deriveFont (Font.BOLD | Font.ITALIC));
2964
                                   } else {
2965
                                       g.setFont (f.deriveFont (Font.BOLD));
2966
                                   }
2967
                                   break;
2968
                              }
2969
                          break;
2970
                        case 'U' :
2971
                        case 'u' : 
2972
                           underline = true;
2973
                           break;
2974
                        case 'f' : 
2975
                        case 'F' : 
2976
                           Color c = findColor (chars, pos, tagEnd);
2977
                           colorStack.push(g.getColor());
2978
                           g.setColor (c);
2979
                           break;
2980
                        case 'P' :
2981
                        case 'p' : 
2982
                            if (style == STYLE_WORDWRAP) {
2983
                               x = origX;
2984
                               int lineHeight=g.getFontMetrics().getHeight();
2985
                               y +=  lineHeight + (lineHeight / 2);
2986
                               heightPainted = y + lineHeight;
2987
                               widthPainted = 0;
2988
                            }
2989
                           break;
2990
                        default  : throwBadHTML (
2991
                            "Malformed or unsupported HTML", pos, chars); //NOI18N
2992
                    }
2993
                }
2994
                
2995
                pos = tagEnd + (done ? 0 : 1);
2996
                inTag = false;
2997
            } else {
2998
                //Okay, we're not in a tag, we need to paint
2999
                
3000
                if (lastWasWhitespace) {
3001
                    //Skip multiple whitespace characters
3002
                    while (Character.isWhitespace (chars[pos])) {
3003
                        pos++;
3004
                    }
3005
                }
3006
                
3007
                //Flag to indicate if an ampersand entity was processed,
3008
                //so the resulting & doesn't get treated as the beginning of
3009
                //another entity (and loop endlessly)
3010
                boolean isAmp=false;
3011
                //Flag to indicate the next found < character really should
3012
                //be painted (it came from an entity), it is not the beginning
3013
                //of a tag
3014
                boolean nextLtIsEntity=false;
3015
                int nextTag = chars.length-1;
3016
                if ((chars[pos] == '&')) {
3017
                    boolean inEntity=pos != chars.length-1;
3018
                    if (inEntity) {
3019
                        int newPos = substEntity(chars, pos+1);
3020
                        inEntity = newPos != -1;
3021
                        if (inEntity) {
3022
                            pos = newPos;
3023
                            isAmp = chars[pos] == '&';
3024
                            //flag it so the next iteration won't think the < 
3025
                            //starts a tag
3026
                            nextLtIsEntity = chars[pos] == '<';
3027
                        } else {
3028
                            nextLtIsEntity = false;
3029
                            isAmp = true;
3030
                        }
3031
                    }
3032
                } else {
3033
                    nextLtIsEntity=false;
3034
                }
3035
                
3036
                for (int i=pos; i < chars.length; i++) {
3037
                    if (((chars[i] == '<') && (!nextLtIsEntity)) || ((chars[i] == '&') && !isAmp)) {
3038
                        nextTag = i-1;
3039
                        break;
3040
                    }
3041
                    //Reset these flags so we don't skip all & or < chars for the rest of the string
3042
                    isAmp = false;
3043
                    nextLtIsEntity=false;
3044
                }
3045
                
3046
                
3047
                FontMetrics fm = g.getFontMetrics(g.getFont());
3048
                //Get the bounds of the substring we'll paint
3049
                Rectangle2D r = fm.getStringBounds(chars, pos, nextTag + 1, g);
3050
                //Store the height, so we can add it if we're in word wrap mode,
3051
                //to return the height painted
3052
                lastHeight = r.getHeight();
3053
                //Work out the length of this tag
3054
                int length = (nextTag + 1) - pos;
3055
                
3056
                //Flag to be set to true if we run out of space
3057
                boolean goToNextRow = false;
3058
                
3059
                //Flag that the current line is longer than the available width,
3060
                //and should be wrapped without finding a word boundary
3061
                boolean brutalWrap = false;
3062
                //Work out the per-character width of the string, for estimating
3063
                //when we'll be out of space and should start the ... in truncate
3064
                //mode
3065
                double chWidth = r.getWidth() / (nextTag - pos);
3066
                //can return this sometimes, so handle it
3067
                if (chWidth == Double.POSITIVE_INFINITY) {
3068
                    chWidth = fm.getMaxAdvance();
3069
                }
3070
                
3071
                if ((style != STYLE_CLIP) && 
3072
                    ((style == STYLE_TRUNCATE && 
3073
                     (widthPainted + r.getWidth() > w - (chWidth * 2)))) || 
3074
                    (style == STYLE_WORDWRAP && 
3075
                     (widthPainted + r.getWidth() > w))) {
3076
                    if (chWidth > 3) {
3077
                        double pixelsOff = (widthPainted + (
3078
                                            r.getWidth() + 5)
3079
                                            ) - w;
3080
                        double estCharsOver = pixelsOff / chWidth;
3081
                        if (style == STYLE_TRUNCATE) {
3082
                            int charsToPaint = new Double((w - widthPainted)
3083
                                / chWidth).intValue();
3084
                            int startPeriodsPos = pos + charsToPaint -3;
3085
                            if (startPeriodsPos >= chars.length) {
3086
                                startPeriodsPos = chars.length - 4;
3087
                            }
3088
                            length = (startPeriodsPos - pos);
3089
                            if (length < 0) length = 0;
3090
                            r = fm.getStringBounds(chars, pos, pos+length, g);
3091
                            truncated = true;
3092
                        } else {
3093
                            goToNextRow = true;
3094
                            int lastChar = new Double(nextTag - 
3095
                                           estCharsOver).intValue();
3096
                                brutalWrap = x == 0;
3097
                                for (int i = lastChar; i > pos; i--) {
3098
                                    lastChar--;
3099
                                    if (Character.isWhitespace (chars[i])) {
3100
                                        length = (lastChar - pos) + 1;
3101
                                        brutalWrap = false;
3102
                                        break;
3103
                                    }
3104
                                }
3105
                                if ((lastChar <= pos) && (length > estCharsOver) 
3106
                                    && !brutalWrap) {
3107
                                    x = origX;
3108
                                    y += r.getHeight();
3109
                                    heightPainted += r.getHeight();
3110
                                    boolean boundsChanged = false;
3111
                                    while (!done && Character.isWhitespace(
3112
                                            chars[pos]) && (pos < nextTag)) {
3113
                                        pos++;
3114
                                        boundsChanged = true;
3115
                                        done = pos == chars.length -1;
3116
                                    }
3117
                                    if (pos == nextTag) {
3118
                                        lastWasWhitespace = true;
3119
                                    }
3120
                                    if (boundsChanged) {
3121
                                        //recalculate the width we will add
3122
                                        r = fm.getStringBounds(chars, pos, 
3123
                                            nextTag + 1, g);
3124
                                    }
3125
                                    goToNextRow = false;
3126
                                    widthPainted = 0;
3127
                                    if (chars[pos - 1 + length] == '<') {
3128
                                        length --;
3129
                                    }
3130
                                } else if (brutalWrap) {
3131
                                    //wrap without checking word boundaries
3132
                                    length = (new Double (
3133
                                         (w - widthPainted) / chWidth)
3134
                                        ).intValue();
3135
                                    if (pos + length > nextTag) {
3136
                                        length = (nextTag - pos);
3137
                                    }
3138
                                    goToNextRow = true;
3139
                                }
3140
                            }
3141
                        }
3142
                    }
3143
                if (!done) {
3144
                    if (paint) {
3145
                        g.drawChars (chars, pos, length, x, y);
3146
                    }
3147
                
3148
                    if ((strikethrough || underline)){
3149
                        LineMetrics lm = fm.getLineMetrics(chars, pos, 
3150
                            length - 1, g);
3151
                        int lineWidth = new Double (x + 
3152
                            r.getWidth()).intValue();
3153
                        if (paint) {
3154
                            if (strikethrough) {
3155
                                int stPos = Math.round (
3156
                                    lm.getStrikethroughOffset()) + 
3157
                                    g.getFont().getBaselineFor(chars[pos]) 
3158
                                    + 1;
3159
//                                int stThick = Math.round (lm.getStrikethroughThickness()); //XXX
3160
                                g.drawLine(x, y + stPos, lineWidth, y + stPos);
3161
                            }
3162
                            if (underline) {
3163
                                int stPos = Math.round (
3164
                                    lm.getUnderlineOffset()) + 
3165
                                    g.getFont().getBaselineFor(chars[pos]) 
3166
                                    + 1;
3167
//                                int stThick = new Float (lm.getUnderlineThickness()).intValue(); //XXX
3168
                                g.drawLine(x, y + stPos, lineWidth, y + stPos);
3169
                            }
3170
                        }
3171
                    }
3172
                    if (goToNextRow) {
3173
                        //if we're in word wrap mode and need to go to the next
3174
                        //line, reconfigure the x and y coordinates
3175
                        x = origX;
3176
                        y += r.getHeight();
3177
                        heightPainted += r.getHeight();
3178
                        widthPainted = 0;
3179
                        pos += (length);
3180
                        //skip any leading whitespace
3181
                        while ((pos < chars.length) && 
3182
                               (Character.isWhitespace(chars[pos])) && 
3183
                               (chars[pos] != '<')) {
3184
                            pos++;
3185
                        }
3186
                        lastWasWhitespace = true;
3187
                        done |= pos >= chars.length;
3188
                    } else {
3189
                        x += r.getWidth();
3190
                        widthPainted += r.getWidth();
3191
                        lastWasWhitespace = Character.isWhitespace (
3192
                            chars[nextTag]);
3193
                        pos = nextTag + 1;
3194
                    }
3195
                    done |= nextTag == chars.length;
3196
                }
3197
            }
3198
        }
3199
        if (style != STYLE_WORDWRAP) {
3200
            return widthPainted;
3201
        } else {
3202
            return heightPainted + lastHeight;
3203
        }
3204
    }    
3205
3206
    private static final boolean strictHTML = Boolean.getBoolean (
3207
        "netbeans.lwhtml.strict"); //NOI18N
3208
    private static Set badStrings=null;
3209
    /** Throw an exception for unsupported or bad html, indicating where the problem is
3210
     * in the message  */
3211
    private static void throwBadHTML (String msg, int pos, char[] chars) {
3212
        char[] chh = new char[pos];
3213
        Arrays.fill (chh, ' '); //NOI18N
3214
        chh[pos-1] = '^'; //NOI18N
3215
        String out = msg + "\n  " + new String (chars) + "\n  " //NOI18N
3216
            + new String(chh) + "\n Full HTML string:" + new String(chars);
3217
        if (!strictHTML) {
3218
            if (badStrings == null) {
3219
                badStrings = new HashSet();
3220
            }
3221
            if (!badStrings.contains(msg)) {
3222
                ErrorManager.getDefault().log(ErrorManager.WARNING,  msg);
3223
                System.err.println(msg); //Also print to stdout - ErrorManager warning will be cut off after first /n
3224
                badStrings.add(msg);
3225
            }
3226
        } else {
3227
            throw new IllegalArgumentException (out); 
3228
        }
3229
    }
3230
    
3231
    /** Parse a font color tag and return an appopriate java.awt.Color instance */
3232
    private static Color findColor (final char[] ch, final int pos, 
3233
                                       final int tagEnd) {
3234
        int colorPos = pos;
3235
        boolean useUIManager = false;
3236
        for (int i=pos; i < tagEnd; i ++) {
3237
            if (ch[i] == 'c') {
3238
                colorPos = i + 6;
3239
                if (ch[colorPos] == '\'' || ch[colorPos] == '"') {
3240
                    colorPos++;
3241
                }
3242
                //skip the leading # character
3243
                if (ch[colorPos] == '#') {
3244
                    colorPos++;
3245
                } else if (ch[colorPos] == '!') {
3246
                    useUIManager = true;
3247
                    colorPos++;
3248
                }
3249
                break;
3250
            }
3251
        }
3252
        if (colorPos == pos) {
3253
            String out = "Could not find color identifier in font declaration";
3254
            throwBadHTML (out, pos, ch);
3255
        }
3256
        //Okay, we're now on the first character of the hex color definition
3257
        String s;
3258
        if (useUIManager) {
3259
            int end = ch.length-1;
3260
            for (int i=colorPos; i < ch.length; i++) {
3261
                if (ch[i] == '"' || ch[i] == '\'') { //NOI18N
3262
                    end = i;
3263
                    break;
3264
                }
3265
            }
3266
            s = new String (ch, colorPos, end-colorPos);
3267
        } else {
3268
            s = new String (ch, colorPos, 6);
3269
        }
3270
        Color result=null;
3271
        if (useUIManager) {
3272
            result = UIManager.getColor (s);
3273
            //Not all look and feels will provide standard colors; handle it gracefully
3274
            if (result == null) {
3275
                throwBadHTML (
3276
                    "Could not resolve logical font declared in HTML: " + s,
3277
                    pos, ch);
3278
                result = UIManager.getColor ("textText"); //NOI18N
3279
                //Avoid NPE in headless situation?
3280
                if (result == null) {
3281
                    result = Color.BLACK;
3282
                }
3283
            }
3284
        } else {
3285
            try {
3286
                int rgb = Integer.parseInt(s, 16);
3287
                result = new Color (rgb);
3288
            } catch (NumberFormatException nfe) {
3289
                throwBadHTML (
3290
                    "Illegal hexadecimal color text: " + s + //NOI18N
3291
                    " in HTML string", colorPos, ch);
3292
            }
3293
        }
3294
        if (result == null) {
3295
            throwBadHTML ("Unresolvable html color: " + s //NOI18N
3296
                + " in HTML string \n  ", pos,  ch);
3297
        }
3298
        return result;
3299
    }
3300
    
3301
    /** Definitions for a limited subset of sgml character entities */
3302
    private static final Object[] entities = new Object[] {
3303
        new char[] {'g','t'}, new char[] {'l','t'}, 
3304
        new char[] {'q','u','o','t'}, new char[] {'a','m','p'}, 
3305
        new char[] {'l','s','q','u','o'},
3306
        new char[] {'r','s','q','u','o'},
3307
        new char[] {'l','d','q','u','o'},
3308
        new char[] {'r','d','q','u','o'},
3309
        new char[] {'n','d','a','s','h'},
3310
        new char[] {'m','d','a','s','h'},
3311
        new char[] {'n','e'},
3312
        new char[] {'l','e'},
3313
        new char[] {'g','e'},
3314
        
3315
        new char[] {'c','o','p','y'},
3316
        new char[] {'r','e','g'},
3317
        new char[] {'t','r','a','d','e'}
3318
        //The rest of the SGML entities are left as an excercise for the reader
3319
    }; //NOI18N
3320
    
3321
    /** Mappings for the array of sgml character entities to characters */
3322
    private static final char[] entitySubstitutions = new char[] {
3323
        '>','<','"','&',8216, 8217, 8220, 8221, 8211, 8212, 8800, 8804, 8805,
3324
        169, 174, 8482
3325
    };
3326
    
3327
    /** Find an entity at the passed character position in the passed array.
3328
     * If an entity is found, the trailing ; character will be substituted
3329
     * with the resulting character, and the position of that character
3330
     * in the array will be returned as the new position to render from,
3331
     * causing the renderer to skip the intervening characters */
3332
    private static final int substEntity(char[] ch, int pos) {
3333
        //There are no 1 character entities, abort
3334
        if (pos >= ch.length-2) {
3335
            return -1;
3336
        }
3337
        //if it's numeric, parse out the number
3338
        if (ch[pos] == '#') {
3339
            return substNumericEntity(ch, pos+1);
3340
        }
3341
        //Okay, we've potentially got a named character entity. Try to find it.
3342
        boolean match;
3343
        for (int i=0; i < entities.length; i++) {
3344
            char[] c = (char[]) entities[i];
3345
            match = true;
3346
            if (c.length < ch.length-pos) {
3347
                for (int j=0; j < c.length; j++) {
3348
                    match &= c[j] == ch[j+pos];
3349
                }
3350
            } else {
3351
                match = false;
3352
            }
3353
            if (match) {
3354
                //if it's a match, we still need the trailing ;
3355
                if (ch[pos+c.length] == ';') {
3356
                    //substitute the character referenced by the entity
3357
                    ch[pos+c.length] = entitySubstitutions[i];
3358
                    return pos+c.length;
3359
                } 
3360
            }
3361
        }
3362
        return -1;
3363
    }
3364
    
3365
    /** Finds a character defined as a numeric entity (e.g. &amp;#8222;)
3366
     * and replaces the trailing ; with the referenced character, returning
3367
     * the position of it so the renderer can continue from there.
3368
     */
3369
    private static final int substNumericEntity(char[] ch, int pos) {
3370
        for (int i=pos; i < ch.length; i++) {
3371
            if (ch[i] == ';') {
3372
                try {
3373
                    ch[i] = (char) Integer.parseInt(
3374
                        new String (ch, pos, i - pos));
3375
                    return i;
3376
                } catch (NumberFormatException nfe) {
3377
                    throwBadHTML("Unparsable numeric entity: " + 
3378
                        new String (ch, pos, i - pos), pos, ch);
3379
                }
3380
            }
3381
        }
3382
        return -1;

Return to bug 29466