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 * @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 }
--- EOF ---