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  */
  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     @SuppressWarnings("serial") // JDK-implementation class
 280     static class BasicEditorKit extends HTMLEditorKit {
 281         /** Shared base style for all documents created by us use. */
 282         private static StyleSheet defaultStyles;
 283 
 284         /**
 285          * Overriden to return our own slimmed down style sheet.
 286          */
 287         public StyleSheet getStyleSheet() {
 288             if (defaultStyles == null) {
 289                 defaultStyles = new StyleSheet();
 290                 StringReader r = new StringReader(styleChanges);
 291                 try {
 292                     defaultStyles.loadRules(r, null);
 293                 } catch (Throwable e) {
 294                     // don't want to die in static initialization...
 295                     // just display things wrong.
 296                 }
 297                 r.close();
 298                 defaultStyles.addStyleSheet(super.getStyleSheet());
 299             }
 300             return defaultStyles;
 301         }
 302 
 303         /**
 304          * Sets the async policy to flush everything in one chunk, and
 305          * to not display unknown tags.
 306          */
 307         public Document createDefaultDocument(Font defaultFont,
 308                                               Color foreground) {
 309             StyleSheet styles = getStyleSheet();
 310             StyleSheet ss = new StyleSheet();
 311             ss.addStyleSheet(styles);
 312             BasicDocument doc = new BasicDocument(ss, defaultFont, foreground);
 313             doc.setAsynchronousLoadPriority(Integer.MAX_VALUE);
 314             doc.setPreservesUnknownTags(false);
 315             return doc;
 316         }
 317 
 318         /**
 319          * Returns the ViewFactory that is used to make sure the Views don't
 320          * load in the background.
 321          */
 322         public ViewFactory getViewFactory() {
 323             return basicHTMLViewFactory;
 324         }
 325     }
 326 
 327 
 328     /**
 329      * BasicHTMLViewFactory extends HTMLFactory to force images to be loaded
 330      * synchronously.
 331      */
 332     static class BasicHTMLViewFactory extends HTMLEditorKit.HTMLFactory {
 333         public View create(Element elem) {
 334             View view = super.create(elem);
 335 
 336             if (view instanceof ImageView) {
 337                 ((ImageView)view).setLoadsSynchronously(true);
 338             }
 339             return view;
 340         }
 341     }
 342 
 343 
 344     /**
 345      * The subclass of HTMLDocument that is used as the model. getForeground
 346      * is overridden to return the foreground property from the Component this
 347      * was created for.
 348      */
 349     @SuppressWarnings("serial") // Superclass is not serializable across versions
 350     static class BasicDocument extends HTMLDocument {
 351         /** The host, that is where we are rendering. */
 352         // private JComponent host;
 353 
 354         BasicDocument(StyleSheet s, Font defaultFont, Color foreground) {
 355             super(s);
 356             setPreservesUnknownTags(false);
 357             setFontAndColor(defaultFont, foreground);
 358         }
 359 
 360         /**
 361          * Sets the default font and default color. These are set by
 362          * adding a rule for the body that specifies the font and color.
 363          * This allows the html to override these should it wish to have
 364          * a custom font or color.
 365          */
 366         private void setFontAndColor(Font font, Color fg) {
 367             getStyleSheet().addRule(sun.swing.SwingUtilities2.
 368                                     displayPropertiesToCSS(font,fg));
 369         }
 370     }
 371 
 372 
 373     /**
 374      * Root text view that acts as an HTML renderer.
 375      */
 376     static class Renderer extends View {
 377 
 378         Renderer(JComponent c, ViewFactory f, View v) {
 379             super(null);
 380             host = c;
 381             factory = f;
 382             view = v;
 383             view.setParent(this);
 384             // initially layout to the preferred size
 385             setSize(view.getPreferredSpan(X_AXIS), view.getPreferredSpan(Y_AXIS));
 386         }
 387 
 388         /**
 389          * Fetches the attributes to use when rendering.  At the root
 390          * level there are no attributes.  If an attribute is resolved
 391          * up the view hierarchy this is the end of the line.
 392          */
 393         public AttributeSet getAttributes() {
 394             return null;
 395         }
 396 
 397         /**
 398          * Determines the preferred span for this view along an axis.
 399          *
 400          * @param axis may be either X_AXIS or Y_AXIS
 401          * @return the span the view would like to be rendered into.
 402          *         Typically the view is told to render into the span
 403          *         that is returned, although there is no guarantee.
 404          *         The parent may choose to resize or break the view.
 405          */
 406         public float getPreferredSpan(int axis) {
 407             if (axis == X_AXIS) {
 408                 // width currently laid out to
 409                 return width;
 410             }
 411             return view.getPreferredSpan(axis);
 412         }
 413 
 414         /**
 415          * Determines the minimum span for this view along an axis.
 416          *
 417          * @param axis may be either X_AXIS or Y_AXIS
 418          * @return the span the view would like to be rendered into.
 419          *         Typically the view is told to render into the span
 420          *         that is returned, although there is no guarantee.
 421          *         The parent may choose to resize or break the view.
 422          */
 423         public float getMinimumSpan(int axis) {
 424             return view.getMinimumSpan(axis);
 425         }
 426 
 427         /**
 428          * Determines the maximum span for this view along an axis.
 429          *
 430          * @param axis may be either X_AXIS or Y_AXIS
 431          * @return the span the view would like to be rendered into.
 432          *         Typically the view is told to render into the span
 433          *         that is returned, although there is no guarantee.
 434          *         The parent may choose to resize or break the view.
 435          */
 436         public float getMaximumSpan(int axis) {
 437             return Integer.MAX_VALUE;
 438         }
 439 
 440         /**
 441          * Specifies that a preference has changed.
 442          * Child views can call this on the parent to indicate that
 443          * the preference has changed.  The root view routes this to
 444          * invalidate on the hosting component.
 445          * <p>
 446          * This can be called on a different thread from the
 447          * event dispatching thread and is basically unsafe to
 448          * propagate into the component.  To make this safe,
 449          * the operation is transferred over to the event dispatching
 450          * thread for completion.  It is a design goal that all view
 451          * methods be safe to call without concern for concurrency,
 452          * and this behavior helps make that true.
 453          *
 454          * @param child the child view
 455          * @param width true if the width preference has changed
 456          * @param height true if the height preference has changed
 457          */
 458         public void preferenceChanged(View child, boolean width, boolean height) {
 459             host.revalidate();
 460             host.repaint();
 461         }
 462 
 463         /**
 464          * Determines the desired alignment for this view along an axis.
 465          *
 466          * @param axis may be either X_AXIS or Y_AXIS
 467          * @return the desired alignment, where 0.0 indicates the origin
 468          *     and 1.0 the full span away from the origin
 469          */
 470         public float getAlignment(int axis) {
 471             return view.getAlignment(axis);
 472         }
 473 
 474         /**
 475          * Renders the view.
 476          *
 477          * @param g the graphics context
 478          * @param allocation the region to render into
 479          */
 480         public void paint(Graphics g, Shape allocation) {
 481             Rectangle alloc = allocation.getBounds();
 482             view.setSize(alloc.width, alloc.height);
 483             view.paint(g, allocation);
 484         }
 485 
 486         /**
 487          * Sets the view parent.
 488          *
 489          * @param parent the parent view
 490          */
 491         public void setParent(View parent) {
 492             throw new Error("Can't set parent on root view");
 493         }
 494 
 495         /**
 496          * Returns the number of views in this view.  Since
 497          * this view simply wraps the root of the view hierarchy
 498          * it has exactly one child.
 499          *
 500          * @return the number of views
 501          * @see #getView
 502          */
 503         public int getViewCount() {
 504             return 1;
 505         }
 506 
 507         /**
 508          * Gets the n-th view in this container.
 509          *
 510          * @param n the number of the view to get
 511          * @return the view
 512          */
 513         public View getView(int n) {
 514             return view;
 515         }
 516 
 517         /**
 518          * Provides a mapping from the document model coordinate space
 519          * to the coordinate space of the view mapped to it.
 520          *
 521          * @param pos the position to convert
 522          * @param a the allocated region to render into
 523          * @return the bounding box of the given position
 524          */
 525         public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
 526             return view.modelToView(pos, a, b);
 527         }
 528 
 529         /**
 530          * Provides a mapping from the document model coordinate space
 531          * to the coordinate space of the view mapped to it.
 532          *
 533          * @param p0 the position to convert >= 0
 534          * @param b0 the bias toward the previous character or the
 535          *  next character represented by p0, in case the
 536          *  position is a boundary of two views.
 537          * @param p1 the position to convert >= 0
 538          * @param b1 the bias toward the previous character or the
 539          *  next character represented by p1, in case the
 540          *  position is a boundary of two views.
 541          * @param a the allocated region to render into
 542          * @return the bounding box of the given position is returned
 543          * @exception BadLocationException  if the given position does
 544          *   not represent a valid location in the associated document
 545          * @exception IllegalArgumentException for an invalid bias argument
 546          * @see View#viewToModel
 547          */
 548         public Shape modelToView(int p0, Position.Bias b0, int p1,
 549                                  Position.Bias b1, Shape a) throws BadLocationException {
 550             return view.modelToView(p0, b0, p1, b1, a);
 551         }
 552 
 553         /**
 554          * Provides a mapping from the view coordinate space to the logical
 555          * coordinate space of the model.
 556          *
 557          * @param x x coordinate of the view location to convert
 558          * @param y y coordinate of the view location to convert
 559          * @param a the allocated region to render into
 560          * @return the location within the model that best represents the
 561          *    given point in the view
 562          */
 563         public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
 564             return view.viewToModel(x, y, a, bias);
 565         }
 566 
 567         /**
 568          * Returns the document model underlying the view.
 569          *
 570          * @return the model
 571          */
 572         public Document getDocument() {
 573             return view.getDocument();
 574         }
 575 
 576         /**
 577          * Returns the starting offset into the model for this view.
 578          *
 579          * @return the starting offset
 580          */
 581         public int getStartOffset() {
 582             return view.getStartOffset();
 583         }
 584 
 585         /**
 586          * Returns the ending offset into the model for this view.
 587          *
 588          * @return the ending offset
 589          */
 590         public int getEndOffset() {
 591             return view.getEndOffset();
 592         }
 593 
 594         /**
 595          * Gets the element that this view is mapped to.
 596          *
 597          * @return the view
 598          */
 599         public Element getElement() {
 600             return view.getElement();
 601         }
 602 
 603         /**
 604          * Sets the view size.
 605          *
 606          * @param width the width
 607          * @param height the height
 608          */
 609         public void setSize(float width, float height) {
 610             this.width = (int) width;
 611             view.setSize(width, height);
 612         }
 613 
 614         /**
 615          * Fetches the container hosting the view.  This is useful for
 616          * things like scheduling a repaint, finding out the host
 617          * components font, etc.  The default implementation
 618          * of this is to forward the query to the parent view.
 619          *
 620          * @return the container
 621          */
 622         public Container getContainer() {
 623             return host;
 624         }
 625 
 626         /**
 627          * Fetches the factory to be used for building the
 628          * various view fragments that make up the view that
 629          * represents the model.  This is what determines
 630          * how the model will be represented.  This is implemented
 631          * to fetch the factory provided by the associated
 632          * EditorKit.
 633          *
 634          * @return the factory
 635          */
 636         public ViewFactory getViewFactory() {
 637             return factory;
 638         }
 639 
 640         private int width;
 641         private View view;
 642         private ViewFactory factory;
 643         private JComponent host;
 644 
 645     }
 646 }