001 /* sillyview : a free model-view-controller system for Java
002 * Copyright (C) 2004 T.J. Willis
003 *
004 * This program is free software; you can redistribute it and/or
005 * modify it under the terms of the GNU General Public License
006 * as published by the Free Software Foundation; either version 2
007 * of the License, or (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, write to the Free Software
016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
017 *
018 * $Header: /cvsroot/sillyview/sillyview/src/net/sourceforge/sillyview/HTMLPane.java,v 1.5 2004/05/22 07:58:27 tj_willis Exp $
019 */
020 package net.sourceforge.sillyview;
021
022 import java.awt.event.*;
023 import java.io.*;
024 import java.net.URL;
025 import java.util.*;
026 import javax.swing.*;
027 import javax.swing.event.HyperlinkListener;
028 import javax.swing.plaf.*;
029 import javax.swing.plaf.metal.*;
030 import javax.swing.text.*;
031 import javax.swing.text.html.*;
032 import net.sourceforge.steelme.*;
033 import org.apache.log4j.*;
034
035 /**
036 * This class is a convenience class for JEditorPanes used to show
037 * HTML content that are un-editable (so as to enable listening for
038 * hyperlink events) and handle UTF-8 characters gracefully. It
039 * wouldn't be strictly necessary were it not for some bugs in the
040 * Java 1.4.2 StyledDocument api (getlength()=0).
041 *
042 * This class keeps its own copy of the EditorPane's text to work
043 * around the getLength, and subsequent getText problems.
044 *
045 * Important to note is that a HTMLPane is aware of and responsive to
046 * Swing Pluggable LookAndFeel colors. JEditorPanes are ignorant of changes
047 * to LookAndFeel. You can plug in your own LookAndFeel to StyleSheet
048 * converter by subclassing LookAndFeelToCSS and using HTMLPane's
049 * setConverter() method. The default BasicLookAndFeelToCSS converter
050 * may well not handle your favorite LookAndFeel (Swing's default Metal
051 * LookAndFeel is supported) , and you may be forced to write a
052 * LookAndFeelToCSS converter for it.
053 *
054 * A "theme" stylesheet (the result of a conversion) does not override
055 * styles implemented directly in the HTML document or included in the
056 * HTML document. It does override all other styles.
057 *
058 * @author <a href="mailto:tj_willis@users.sourceforge.net">T.J. Willis</a>
059 * @version 1.0
060 */
061 public class HTMLPane extends JEditorPane
062 {
063 private String dString;
064 private LookAndFeelToCSS converter = new BasicLookAndFeelToCSS ();
065 private volatile boolean initialized = false;
066 private Category cat
067 = Category.getInstance(HTMLPane.class.getName());
068 private boolean autodump = false;
069
070 /**
071 * Creates a new <code>HTMLPane</code> instance with no text.
072 *
073 */
074 public HTMLPane () {
075 this("");
076 //initialized = true;
077 }
078
079 /**
080 * Creates a new <code>HTMLPane</code> instance with the given text.
081 * Default text encoding is "text/html; UTF-8".
082 * @param text a <code>String</code> value
083 */
084 public HTMLPane (final String text) {
085 this(text != null ? text : "" ,null);
086 initialized = true;
087 }
088
089 /**
090 * Creates a new <code>HTMLPane</code> instance with the given
091 * stylesheet. Default text encoding is "text/html; UTF-8".
092 *
093 * @param theme a <code>StyleSheet</code> value
094 */
095 public HTMLPane (final StyleSheet theme) {
096 this("",theme);
097 //initialized = true;
098 }
099
100 /**
101 * Creates a new <code>HTMLPane</code> instance with the given
102 * text and stylesheet. Default text encoding is "text/html; UTF-8".
103 *
104 * @param text a <code>String</code> value
105 * @param theme a <code>StyleSheet</code> value
106 */
107 public HTMLPane (final String text, final StyleSheet theme) {
108 super ("text/html; UTF-8", text!= null ? text : "");
109 putClientProperty("charset","UTF-8");
110 initialized = true;
111 dString = text;
112 init ();
113 if(theme!=null){
114 // FIXME: overridable method
115 this.setTheme (theme);
116 }
117 }
118
119 /**
120 * Creates a new <code>HTMLPane</code> instance showing the HTML
121 * page at the given URL and with no accessory stylesheet.
122 * Default text encoding is "text/html; UTF-8".
123 *
124 * @param initialPage an <code>URL</code> value
125 * @exception IOException if an error occurs
126 */
127 public HTMLPane (final URL initialPage) throws IOException {
128 this(initialPage,null);
129 initialized = true;
130 }
131
132 /**
133 * Creates a new <code>HTMLPane</code> instance showing the HTML page
134 * at the given URL and with the given accesory stylesheet.
135 * Default text encoding is "text/html; UTF-8".
136 *
137 * @param initialPage an <code>URL</code> value
138 * @param theme a <code>StyleSheet</code> value
139 * @exception IOException if an error occurs
140 */
141 public HTMLPane (URL initialPage, StyleSheet theme) throws IOException {
142 super ("text/html; UTF-8", "");
143 initialized = true;
144 dString = getDocument (initialPage);
145 init ();
146 bugFixSetText (dString!=null ? dString : "");
147 if(theme!=null){
148 // FIXME: overridable method
149 this.setTheme (theme);
150 }
151 }
152
153 /**
154 * Sets the pane to non-editable, the content type to "text/html; UTF-8"
155 * and deactivates "autoformsubmission" so that HyperlinkListeners can
156 * handle form submission events.
157 *
158 */
159 private final void init () {
160 setEditable (false);
161 setContentType ("text/html; UTF-8");
162 HTMLEditorKit kitten = getHTMLEditorKit();
163 // jre1.5 --
164 // ensures that clicking a form submit btn creates a HyperlinkEvent
165 kitten.setAutoFormSubmission (false);
166 }
167
168
169
170 /**
171 * Returns the stylesheet for the current document.
172 *
173 * @return a <code>StyleSheet</code> value
174 */
175 protected StyleSheet getDocumentStyleSheet () {
176 //if(cat!=null)cat.debug("Entering getDocumentStyleSheet");
177 Document doc = getDocument ();
178 if (doc==null || !(doc instanceof HTMLDocument)){
179 return null;
180 }
181 HTMLDocument htmld = (HTMLDocument) doc;
182 //if(cat!=null)cat.debug("Leaving getDocumentStyleSheet");
183 return htmld.getStyleSheet ();
184 }
185
186 /**
187 * Returns the HTML document at the given URL as a String.
188 *
189 * @param documentURL a <code>String</code> value
190 * @return a <code>String</code> value
191 * @exception java.net.MalformedURLException if an error occurs
192 * @exception java.io.IOException if an error occurs
193 */
194 private static String getDocument (final String documentURL)
195 throws java.net.MalformedURLException, java.io.IOException
196 {
197 URL url = new URL (documentURL);
198 if (url == null){
199 return null;
200 }
201 return getDocument (url);
202 }
203
204 /**
205 * Returns the HTML document at the given URL as a String.
206 *
207 * @param u an <code>URL</code> value
208 * @return a <code>String</code> value
209 * @exception java.io.IOException if an error occurs
210 */
211 protected static String getDocument (final URL u)
212 throws java.io.IOException
213 {
214 StringBuffer cont = new StringBuffer ();
215
216 InputStream is = u.openStream ();
217 InputStreamReader isr = new InputStreamReader (is);
218 BufferedReader in = new BufferedReader (isr);
219 String foo = in.readLine ();
220 while (foo != null) {
221 cont.append (foo).append ("\n");
222 //cont.append("\n"); //cont += foo +"\n";
223 foo = in.readLine ();
224 }
225 //is.flush();
226 is.close();
227 return cont.toString ();
228 }
229
230 /**
231 * Adds the stylesheet at the given url as a "theme," i.e. it will
232 * not override rules in the document's own stylesheet.
233 *
234 * @param styleSheetUrl a <code>String</code> value
235 * @exception java.net.MalformedURLException if an error occurs
236 */
237 protected void setTheme (String styleSheetUrl)
238 throws java.net.MalformedURLException {
239 if (styleSheetUrl == null) {
240 this.setTheme ((URL) null);
241 return;
242 }
243
244 URL ssurl = new URL (styleSheetUrl);
245 this.setTheme (ssurl);
246 }
247
248 /**
249 * Adds the stylesheet at the given url as a "theme," i.e. it will
250 * not override rules in the document's own stylesheet.
251 *
252 * @param styleSheetUrl an <code>URL</code> value
253 */
254 private void setTheme (final URL styleSheetUrl) {
255 StyleSheet newSheet = new StyleSheet ();
256 newSheet.importStyleSheet (styleSheetUrl);
257 this.setTheme (newSheet);
258 }
259
260 /**
261 * Adds the stylesheet at the given url as a "theme," i.e. it will
262 * not override rules in the document's own stylesheet.
263 *
264 * @param s2 a <code>StyleSheet</code> value
265 */
266 private void setTheme (final StyleSheet s2) {
267 try {
268 bugFixSetText (dString);
269
270 if(s2!=null) {
271 StyleSheet styles = getDocumentStyleSheet ();
272 if(styles!=null) {
273 styles.addStyleSheet(s2);
274 } else {
275 styles = s2;
276 }
277 final HTMLEditorKit kitten = getHTMLEditorKit();
278 if(kitten!=null){
279 kitten.setStyleSheet (styles);
280 kitten.setAutoFormSubmission (false);
281 }
282 }
283 if(autodump){
284 dump();
285 }
286 } catch (Exception e) {
287 if(cat!=null){
288 cat.error("Odd HTMLPane exception #1: ",e);
289 }
290 }
291 }
292
293
294 /**
295 * Returns the editorkit if it is a HTMLEditorKit, or null otherwise.
296 */
297 private HTMLEditorKit getHTMLEditorKit(){
298 EditorKit newKit = getEditorKit ();
299 if (!(newKit instanceof HTMLEditorKit)){
300 return null;
301 }
302 return (HTMLEditorKit) newKit;
303 }
304
305
306 /**
307 * Sets the text to the given value, tearing down the
308 * JEditorPane's backing model, which prevents peculiarities.
309 * See the documentation for JEditorPane.setText() for more
310 * information.
311 */
312 private final void bugFixSetText(final String text)
313 {
314 dString = (text != null? text : "");
315 StringReader rdr = null;
316 Document d0 = null;
317 rdr = new StringReader(dString);
318 d0 = getDocument ();
319
320 // had some exceptions in super constructor
321 if(rdr == null || d0==null){
322 if(initialized){
323 super.setText(dString);
324 }
325 return;
326 }
327 try {
328 HTMLDocument hDoc = (HTMLDocument) d0;
329 read(rdr,hDoc);
330 } catch (Exception e) {
331 cat.error("bugFixSetText exception: ",e);
332 }
333 }
334
335 /**
336 * Sets the text and clears and clears any applied "theme"
337 * stylesheets.
338 *
339 * @param text a <code>String</code> value
340 */
341 public final void setText (final String text) {
342 //bugFixSetText(text);
343 dString = (text !=null ? text : "");
344 try {
345 this.setTheme ((StyleSheet) null);
346 } catch (Exception e) {
347 cat.error("Odd HTMLPane exception #2: ",e);
348 // "can't" happen
349 }
350 }
351
352 // /**
353 // * Sets the text to the HTML document at the given URL.
354 // *
355 // * @param textUrl a <code>String</code> value
356 // * @exception java.net.MalformedURLException if an error occurs
357 // * @exception java.io.IOException if an error occurs
358 // */
359 // private void setTextUrl (final String textUrl)
360 // throws java.net.MalformedURLException, java.io.IOException {
361 // if (textUrl == null) {
362 // this.setText ("");
363 // return;
364 // }
365 // URL u = null;
366 // u = new URL (textUrl);
367 // this.setText (u);
368 // }
369
370 /**
371 * Sets the text to the HTML document at the given URL.
372 *
373 * @param textURL an <code>URL</code> value
374 * @exception java.io.IOException if an error occurs
375 */
376 private void setText (final URL textURL)
377 throws java.io.IOException
378 {
379 StringBuffer cont = new StringBuffer ();
380
381 InputStream is = textURL.openStream ();
382 InputStreamReader isr = new InputStreamReader (is);
383 BufferedReader in = new BufferedReader (isr);
384 String foo = in.readLine ();
385 while (foo != null) {
386 cont.append (foo).append ("\n"); // += foo +"\n";
387 foo = in.readLine ();
388 }
389 //is.flush();
390 is.close();
391 bugFixSetText (cont.toString ());
392
393 }
394
395 /**
396 * Dumps the current stylesheet for debugging.
397 *
398 */
399 public final void dump () {
400 HTMLEditorKit kitten = getHTMLEditorKit();
401 dump (kitten.getStyleSheet ());
402 }
403
404 /**
405 * Setting autoDump to true causes the stylesheet to be printed
406 * every time it is changed.
407 *
408 * @param b a <code>boolean</code> value
409 */
410 public void setAutoDump(boolean newAutoDump){
411 autodump = newAutoDump;
412 }
413
414 /**
415 * Sends all style rules to be printed out to System.out.
416 *
417 * @param styles a <code>StyleSheet</code> value
418 */
419 public final void dump (final StyleSheet styles) {
420 Enumeration rules = styles.getStyleNames ();
421 while (rules.hasMoreElements ()) {
422 String name = (String) rules.nextElement ();
423 Style rule = styles.getStyle (name);
424 System.out.println (rule.toString ());
425 }
426
427 }
428
429 private volatile LookAndFeel lastLookAndFeel = null;
430 private volatile javax.swing.plaf.TextUI lastUI = null;
431
432 /**
433 * Describe <code>setUI</code> method here.
434 *
435 * @param newui a <code>javax.swing.plaf.TextUI</code> value
436 */
437 public void setUI(javax.swing.plaf.TextUI newui) {
438 //if(cat!=null)cat.debug("Entering setUI");
439 if(lastUI==newui) {
440 //if(cat!=null)cat.debug("Leaving setUI");
441 return;
442 }
443 LookAndFeel lf = UIManager.getLookAndFeel ();
444 if(lf!=null && lf != lastLookAndFeel ){
445 StyleSheet s = getStyleSheet (lf);
446 this.setTheme (s);
447 }
448 lastLookAndFeel = lf;
449 super.setUI(newui);
450 lastUI = newui;
451 //if(cat!=null)cat.debug("Leaving setUI");
452 }
453
454 // private volatile int updateLoop = 0;
455
456 // /**
457 // * Describe <code>updateUI</code> method here.
458 // *
459 // */
460 // public void updateUI () {
461 // // seems to be an infinite loop after a few updateUI's
462 // if(updateLoop>5) {
463 // return;
464 // }
465 // updateLoop++;
466 // System.out.println ("UpdateUI called: " + updateLoop);
467
468 // LookAndFeel lf = UIManager.getLookAndFeel ();
469 // if(lf!=null && lf != lastLookAndFeel ){
470 // StyleSheet s = getStyleSheet (lf);
471 // setTheme (s);
472 // }
473 // lastLookAndFeel = lf;
474 // super.updateUI ();
475 // updateLoop--;
476 // }
477
478 /**
479 * Describe <code>getConverter</code> method here.
480 *
481 * @return a <code>LookAndFeelToCSS</code> value
482 */
483 public final LookAndFeelToCSS getConverter () {
484 return converter;
485 }
486
487 /**
488 * Allows you to plug in a LookAndFeel to CSS converter of your
489 * choice.
490 *
491 * @param conv a <code>LookAndFeelToCSS</code> value
492 */
493 public final void setConverter (final LookAndFeelToCSS conv) {
494 converter = conv;
495 }
496
497 private StyleSheet getStyleSheet (final LookAndFeel mlf) {
498 if (converter == null){
499 converter = new BasicLookAndFeelToCSS ();
500 }
501 assert converter != null:"Converter is null";
502 StyleSheet s = null;
503 try {
504 s = converter.convert (mlf);
505 } catch (UnsupportedLookAndFeelException e) {
506 cat.warn ("Exception : ", e);
507 } catch (NullPointerException ex) {
508 cat.warn ("Exception : ",ex);
509 ex.printStackTrace ();
510 }
511 return s;
512 }
513
514 /**
515 * Add a HyperlinkListener to this pane. This allows you to handle
516 * hyperlink events as well as form submission events.
517 *
518 * @param l a <code>HyperlinkListener</code> value
519 */
520 public void addHyperlinkListener (HyperlinkListener listener) {
521 super.addHyperlinkListener (listener);
522 }
523
524 /**
525 * Tries to do super.doLayout() and warns if this causes an exception.
526 *
527 */
528 public void doLayout( ) {
529 //cat.debug("Entering doLayout");
530 try {
531 super.doLayout();
532 } catch (Exception e) {
533 cat.warn("Caught odd layout exception: ",e);
534 }
535 //cat.debug("Leaving doLayout");
536 }
537
538 public void forceValidate(){
539 validateTree();
540 }
541 }