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 }