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