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