blob: 0aaea752e49cb38679462d4e45a7b4e7d28c1417 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any 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 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}