1/*
2 * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package javax.swing.plaf.basic;
26
27import java.io.*;
28import java.awt.*;
29import java.net.URL;
30
31import javax.swing.*;
32import javax.swing.text.*;
33import javax.swing.text.html.*;
34
35import sun.swing.SwingUtilities2;
36
37/**
38 * Support for providing html views for the swing components.
39 * This translates a simple html string to a javax.swing.text.View
40 * implementation that can render the html and provide the necessary
41 * layout semantics.
42 *
43 * @author  Timothy Prinzing
44 * @since 1.3
45 */
46public class BasicHTML {
47
48    /**
49     * Create an html renderer for the given component and
50     * string of html.
51     *
52     * @param c a component
53     * @param html an HTML string
54     * @return an HTML renderer
55     */
56    public static View createHTMLView(JComponent c, String html) {
57        BasicEditorKit kit = getFactory();
58        Document doc = kit.createDefaultDocument(c.getFont(),
59                                                 c.getForeground());
60        Object base = c.getClientProperty(documentBaseKey);
61        if (base instanceof URL) {
62            ((HTMLDocument)doc).setBase((URL)base);
63        }
64        Reader r = new StringReader(html);
65        try {
66            kit.read(r, doc, 0);
67        } catch (Throwable e) {
68        }
69        ViewFactory f = kit.getViewFactory();
70        View hview = f.create(doc.getDefaultRootElement());
71        View v = new Renderer(c, f, hview);
72        return v;
73    }
74
75    /**
76     * Returns the baseline for the html renderer.
77     *
78     * @param view the View to get the baseline for
79     * @param w the width to get the baseline for
80     * @param h the height to get the baseline for
81     * @throws IllegalArgumentException if width or height is < 0
82     * @return baseline or a value < 0 indicating there is no reasonable
83     *                  baseline
84     * @see java.awt.FontMetrics
85     * @see javax.swing.JComponent#getBaseline(int,int)
86     * @since 1.6
87     */
88    public static int getHTMLBaseline(View view, int w, int h) {
89        if (w < 0 || h < 0) {
90            throw new IllegalArgumentException(
91                    "Width and height must be >= 0");
92        }
93        if (view instanceof Renderer) {
94            return getBaseline(view.getView(0), w, h);
95        }
96        return -1;
97    }
98
99    /**
100     * Gets the baseline for the specified component.  This digs out
101     * the View client property, and if non-null the baseline is calculated
102     * from it.  Otherwise the baseline is the value <code>y + ascent</code>.
103     */
104    static int getBaseline(JComponent c, int y, int ascent,
105                                  int w, int h) {
106        View view = (View)c.getClientProperty(BasicHTML.propertyKey);
107        if (view != null) {
108            int baseline = getHTMLBaseline(view, w, h);
109            if (baseline < 0) {
110                return baseline;
111            }
112            return y + baseline;
113        }
114        return y + ascent;
115    }
116
117    /**
118     * Gets the baseline for the specified View.
119     */
120    static int getBaseline(View view, int w, int h) {
121        if (hasParagraph(view)) {
122            view.setSize(w, h);
123            return getBaseline(view, new Rectangle(0, 0, w, h));
124        }
125        return -1;
126    }
127
128    private static int getBaseline(View view, Shape bounds) {
129        if (view.getViewCount() == 0) {
130            return -1;
131        }
132        AttributeSet attributes = view.getElement().getAttributes();
133        Object name = null;
134        if (attributes != null) {
135            name = attributes.getAttribute(StyleConstants.NameAttribute);
136        }
137        int index = 0;
138        if (name == HTML.Tag.HTML && view.getViewCount() > 1) {
139            // For html on widgets the header is not visible, skip it.
140            index++;
141        }
142        bounds = view.getChildAllocation(index, bounds);
143        if (bounds == null) {
144            return -1;
145        }
146        View child = view.getView(index);
147        if (view instanceof javax.swing.text.ParagraphView) {
148            Rectangle rect;
149            if (bounds instanceof Rectangle) {
150                rect = (Rectangle)bounds;
151            }
152            else {
153                rect = bounds.getBounds();
154            }
155            return rect.y + (int)(rect.height *
156                                  child.getAlignment(View.Y_AXIS));
157        }
158        return getBaseline(child, bounds);
159    }
160
161    private static boolean hasParagraph(View view) {
162        if (view instanceof javax.swing.text.ParagraphView) {
163            return true;
164        }
165        if (view.getViewCount() == 0) {
166            return false;
167        }
168        AttributeSet attributes = view.getElement().getAttributes();
169        Object name = null;
170        if (attributes != null) {
171            name = attributes.getAttribute(StyleConstants.NameAttribute);
172        }
173        int index = 0;
174        if (name == HTML.Tag.HTML && view.getViewCount() > 1) {
175            // For html on widgets the header is not visible, skip it.
176            index = 1;
177        }
178        return hasParagraph(view.getView(index));
179    }
180
181    /**
182     * Check the given string to see if it should trigger the
183     * html rendering logic in a non-text component that supports
184     * html rendering.
185     *
186     * @param s a text
187     * @return {@code true} if the given string should trigger the
188     *         html rendering logic in a non-text component
189     */
190    public static boolean isHTMLString(String s) {
191        if (s != null) {
192            if ((s.length() >= 6) && (s.charAt(0) == '<') && (s.charAt(5) == '>')) {
193                String tag = s.substring(1,5);
194                return tag.equalsIgnoreCase(propertyKey);
195            }
196        }
197        return false;
198    }
199
200    /**
201     * Stash the HTML render for the given text into the client
202     * properties of the given JComponent. If the given text is
203     * <em>NOT HTML</em> the property will be cleared of any
204     * renderer.
205     * <p>
206     * This method is useful for ComponentUI implementations
207     * that are static (i.e. shared) and get their state
208     * entirely from the JComponent.
209     *
210     * @param c a component
211     * @param text a text
212     */
213    public static void updateRenderer(JComponent c, String text) {
214        View value = null;
215        View oldValue = (View)c.getClientProperty(BasicHTML.propertyKey);
216        Boolean htmlDisabled = (Boolean) c.getClientProperty(htmlDisable);
217        if (htmlDisabled != Boolean.TRUE && BasicHTML.isHTMLString(text)) {
218            value = BasicHTML.createHTMLView(c, text);
219        }
220        if (value != oldValue && oldValue != null) {
221            for (int i = 0; i < oldValue.getViewCount(); i++) {
222                oldValue.getView(i).setParent(null);
223            }
224        }
225        c.putClientProperty(BasicHTML.propertyKey, value);
226    }
227
228    /**
229     * If this client property of a JComponent is set to Boolean.TRUE
230     * the component's 'text' property is never treated as HTML.
231     */
232    private static final String htmlDisable = "html.disable";
233
234    /**
235     * Key to use for the html renderer when stored as a
236     * client property of a JComponent.
237     */
238    public static final String propertyKey = "html";
239
240    /**
241     * Key stored as a client property to indicate the base that relative
242     * references are resolved against. For example, lets say you keep
243     * your images in the directory resources relative to the code path,
244     * you would use the following the set the base:
245     * <pre>
246     *   jComponent.putClientProperty(documentBaseKey,
247     *                                xxx.class.getResource("resources/"));
248     * </pre>
249     */
250    public static final String documentBaseKey = "html.base";
251
252    static BasicEditorKit getFactory() {
253        if (basicHTMLFactory == null) {
254            basicHTMLViewFactory = new BasicHTMLViewFactory();
255            basicHTMLFactory = new BasicEditorKit();
256        }
257        return basicHTMLFactory;
258    }
259
260    /**
261     * The source of the html renderers
262     */
263    private static BasicEditorKit basicHTMLFactory;
264
265    /**
266     * Creates the Views that visually represent the model.
267     */
268    private static ViewFactory basicHTMLViewFactory;
269
270    /**
271     * Overrides to the default stylesheet.  Should consider
272     * just creating a completely fresh stylesheet.
273     */
274    private static final String styleChanges =
275    "p { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0 }" +
276    "body { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0 }";
277
278    /**
279     * The views produced for the ComponentUI implementations aren't
280     * going to be edited and don't need full html support.  This kit
281     * alters the HTMLEditorKit to try and trim things down a bit.
282     * It does the following:
283     * <ul>
284     * <li>It doesn't produce Views for things like comments,
285     * head, title, unknown tags, etc.
286     * <li>It installs a different set of css settings from the default
287     * provided by HTMLEditorKit.
288     * </ul>
289     */
290    @SuppressWarnings("serial") // JDK-implementation class
291    static class BasicEditorKit extends HTMLEditorKit {
292        /** Shared base style for all documents created by us use. */
293        private static StyleSheet defaultStyles;
294
295        /**
296         * Overriden to return our own slimmed down style sheet.
297         */
298        public StyleSheet getStyleSheet() {
299            if (defaultStyles == null) {
300                defaultStyles = new StyleSheet();
301                StringReader r = new StringReader(styleChanges);
302                try {
303                    defaultStyles.loadRules(r, null);
304                } catch (Throwable e) {
305                    // don't want to die in static initialization...
306                    // just display things wrong.
307                }
308                r.close();
309                defaultStyles.addStyleSheet(super.getStyleSheet());
310            }
311            return defaultStyles;
312        }
313
314        /**
315         * Sets the async policy to flush everything in one chunk, and
316         * to not display unknown tags.
317         */
318        public Document createDefaultDocument(Font defaultFont,
319                                              Color foreground) {
320            StyleSheet styles = getStyleSheet();
321            StyleSheet ss = new StyleSheet();
322            ss.addStyleSheet(styles);
323            BasicDocument doc = new BasicDocument(ss, defaultFont, foreground);
324            doc.setAsynchronousLoadPriority(Integer.MAX_VALUE);
325            doc.setPreservesUnknownTags(false);
326            return doc;
327        }
328
329        /**
330         * Returns the ViewFactory that is used to make sure the Views don't
331         * load in the background.
332         */
333        public ViewFactory getViewFactory() {
334            return basicHTMLViewFactory;
335        }
336    }
337
338
339    /**
340     * BasicHTMLViewFactory extends HTMLFactory to force images to be loaded
341     * synchronously.
342     */
343    static class BasicHTMLViewFactory extends HTMLEditorKit.HTMLFactory {
344        public View create(Element elem) {
345            View view = super.create(elem);
346
347            if (view instanceof ImageView) {
348                ((ImageView)view).setLoadsSynchronously(true);
349            }
350            return view;
351        }
352    }
353
354
355    /**
356     * The subclass of HTMLDocument that is used as the model. getForeground
357     * is overridden to return the foreground property from the Component this
358     * was created for.
359     */
360    @SuppressWarnings("serial") // Superclass is not serializable across versions
361    static class BasicDocument extends HTMLDocument {
362        /** The host, that is where we are rendering. */
363        // private JComponent host;
364
365        BasicDocument(StyleSheet s, Font defaultFont, Color foreground) {
366            super(s);
367            setPreservesUnknownTags(false);
368            setFontAndColor(defaultFont, foreground);
369        }
370
371        /**
372         * Sets the default font and default color. These are set by
373         * adding a rule for the body that specifies the font and color.
374         * This allows the html to override these should it wish to have
375         * a custom font or color.
376         */
377        private void setFontAndColor(Font font, Color fg) {
378            getStyleSheet().addRule(sun.swing.SwingUtilities2.
379                                    displayPropertiesToCSS(font,fg));
380        }
381    }
382
383
384    /**
385     * Root text view that acts as an HTML renderer.
386     */
387    static class Renderer extends View {
388
389        Renderer(JComponent c, ViewFactory f, View v) {
390            super(null);
391            host = c;
392            factory = f;
393            view = v;
394            view.setParent(this);
395            // initially layout to the preferred size
396            setSize(view.getPreferredSpan(X_AXIS), view.getPreferredSpan(Y_AXIS));
397        }
398
399        /**
400         * Fetches the attributes to use when rendering.  At the root
401         * level there are no attributes.  If an attribute is resolved
402         * up the view hierarchy this is the end of the line.
403         */
404        public AttributeSet getAttributes() {
405            return null;
406        }
407
408        /**
409         * Determines the preferred span for this view along an axis.
410         *
411         * @param axis may be either X_AXIS or Y_AXIS
412         * @return the span the view would like to be rendered into.
413         *         Typically the view is told to render into the span
414         *         that is returned, although there is no guarantee.
415         *         The parent may choose to resize or break the view.
416         */
417        public float getPreferredSpan(int axis) {
418            if (axis == X_AXIS) {
419                // width currently laid out to
420                return width;
421            }
422            return view.getPreferredSpan(axis);
423        }
424
425        /**
426         * Determines the minimum span for this view along an axis.
427         *
428         * @param axis may be either X_AXIS or Y_AXIS
429         * @return the span the view would like to be rendered into.
430         *         Typically the view is told to render into the span
431         *         that is returned, although there is no guarantee.
432         *         The parent may choose to resize or break the view.
433         */
434        public float getMinimumSpan(int axis) {
435            return view.getMinimumSpan(axis);
436        }
437
438        /**
439         * Determines the maximum span for this view along an axis.
440         *
441         * @param axis may be either X_AXIS or Y_AXIS
442         * @return the span the view would like to be rendered into.
443         *         Typically the view is told to render into the span
444         *         that is returned, although there is no guarantee.
445         *         The parent may choose to resize or break the view.
446         */
447        public float getMaximumSpan(int axis) {
448            return Integer.MAX_VALUE;
449        }
450
451        /**
452         * Specifies that a preference has changed.
453         * Child views can call this on the parent to indicate that
454         * the preference has changed.  The root view routes this to
455         * invalidate on the hosting component.
456         * <p>
457         * This can be called on a different thread from the
458         * event dispatching thread and is basically unsafe to
459         * propagate into the component.  To make this safe,
460         * the operation is transferred over to the event dispatching
461         * thread for completion.  It is a design goal that all view
462         * methods be safe to call without concern for concurrency,
463         * and this behavior helps make that true.
464         *
465         * @param child the child view
466         * @param width true if the width preference has changed
467         * @param height true if the height preference has changed
468         */
469        public void preferenceChanged(View child, boolean width, boolean height) {
470            host.revalidate();
471            host.repaint();
472        }
473
474        /**
475         * Determines the desired alignment for this view along an axis.
476         *
477         * @param axis may be either X_AXIS or Y_AXIS
478         * @return the desired alignment, where 0.0 indicates the origin
479         *     and 1.0 the full span away from the origin
480         */
481        public float getAlignment(int axis) {
482            return view.getAlignment(axis);
483        }
484
485        /**
486         * Renders the view.
487         *
488         * @param g the graphics context
489         * @param allocation the region to render into
490         */
491        public void paint(Graphics g, Shape allocation) {
492            Rectangle alloc = allocation.getBounds();
493            view.setSize(alloc.width, alloc.height);
494            view.paint(g, allocation);
495        }
496
497        /**
498         * Sets the view parent.
499         *
500         * @param parent the parent view
501         */
502        public void setParent(View parent) {
503            throw new Error("Can't set parent on root view");
504        }
505
506        /**
507         * Returns the number of views in this view.  Since
508         * this view simply wraps the root of the view hierarchy
509         * it has exactly one child.
510         *
511         * @return the number of views
512         * @see #getView
513         */
514        public int getViewCount() {
515            return 1;
516        }
517
518        /**
519         * Gets the n-th view in this container.
520         *
521         * @param n the number of the view to get
522         * @return the view
523         */
524        public View getView(int n) {
525            return view;
526        }
527
528        /**
529         * Provides a mapping from the document model coordinate space
530         * to the coordinate space of the view mapped to it.
531         *
532         * @param pos the position to convert
533         * @param a the allocated region to render into
534         * @return the bounding box of the given position
535         */
536        public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
537            return view.modelToView(pos, a, b);
538        }
539
540        /**
541         * Provides a mapping from the document model coordinate space
542         * to the coordinate space of the view mapped to it.
543         *
544         * @param p0 the position to convert >= 0
545         * @param b0 the bias toward the previous character or the
546         *  next character represented by p0, in case the
547         *  position is a boundary of two views.
548         * @param p1 the position to convert >= 0
549         * @param b1 the bias toward the previous character or the
550         *  next character represented by p1, in case the
551         *  position is a boundary of two views.
552         * @param a the allocated region to render into
553         * @return the bounding box of the given position is returned
554         * @exception BadLocationException  if the given position does
555         *   not represent a valid location in the associated document
556         * @exception IllegalArgumentException for an invalid bias argument
557         * @see View#viewToModel
558         */
559        public Shape modelToView(int p0, Position.Bias b0, int p1,
560                                 Position.Bias b1, Shape a) throws BadLocationException {
561            return view.modelToView(p0, b0, p1, b1, a);
562        }
563
564        /**
565         * Provides a mapping from the view coordinate space to the logical
566         * coordinate space of the model.
567         *
568         * @param x x coordinate of the view location to convert
569         * @param y y coordinate of the view location to convert
570         * @param a the allocated region to render into
571         * @return the location within the model that best represents the
572         *    given point in the view
573         */
574        public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
575            return view.viewToModel(x, y, a, bias);
576        }
577
578        /**
579         * Returns the document model underlying the view.
580         *
581         * @return the model
582         */
583        public Document getDocument() {
584            return view.getDocument();
585        }
586
587        /**
588         * Returns the starting offset into the model for this view.
589         *
590         * @return the starting offset
591         */
592        public int getStartOffset() {
593            return view.getStartOffset();
594        }
595
596        /**
597         * Returns the ending offset into the model for this view.
598         *
599         * @return the ending offset
600         */
601        public int getEndOffset() {
602            return view.getEndOffset();
603        }
604
605        /**
606         * Gets the element that this view is mapped to.
607         *
608         * @return the view
609         */
610        public Element getElement() {
611            return view.getElement();
612        }
613
614        /**
615         * Sets the view size.
616         *
617         * @param width the width
618         * @param height the height
619         */
620        public void setSize(float width, float height) {
621            this.width = (int) width;
622            view.setSize(width, height);
623        }
624
625        /**
626         * Fetches the container hosting the view.  This is useful for
627         * things like scheduling a repaint, finding out the host
628         * components font, etc.  The default implementation
629         * of this is to forward the query to the parent view.
630         *
631         * @return the container
632         */
633        public Container getContainer() {
634            return host;
635        }
636
637        /**
638         * Fetches the factory to be used for building the
639         * various view fragments that make up the view that
640         * represents the model.  This is what determines
641         * how the model will be represented.  This is implemented
642         * to fetch the factory provided by the associated
643         * EditorKit.
644         *
645         * @return the factory
646         */
647        public ViewFactory getViewFactory() {
648            return factory;
649        }
650
651        private int width;
652        private View view;
653        private ViewFactory factory;
654        private JComponent host;
655
656    }
657}
658