001/* A viewer for HTML files. 002 003 Copyright (c) 2000-2014 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 027 */ 028package ptolemy.actor.gui; 029 030import java.awt.Dimension; 031import java.awt.Graphics; 032import java.awt.Graphics2D; 033import java.awt.geom.AffineTransform; 034import java.awt.print.PageFormat; 035import java.awt.print.Printable; 036import java.awt.print.PrinterException; 037import java.io.File; 038import java.io.IOException; 039import java.lang.reflect.Field; 040import java.lang.reflect.InvocationTargetException; 041import java.lang.reflect.Method; 042import java.net.MalformedURLException; 043import java.net.URL; 044 045import javax.swing.BoxLayout; 046import javax.swing.JEditorPane; 047import javax.swing.JScrollPane; 048import javax.swing.event.HyperlinkEvent; 049import javax.swing.event.HyperlinkListener; 050import javax.swing.text.html.HTMLDocument; 051import javax.swing.text.html.HTMLEditorKit; 052import javax.swing.text.html.HTMLFrameHyperlinkEvent; 053import javax.swing.text.html.StyleSheet; 054 055import ptolemy.gui.Top; 056import ptolemy.kernel.util.IllegalActionException; 057import ptolemy.kernel.util.StringAttribute; 058import ptolemy.util.ClassUtilities; 059import ptolemy.util.FileUtilities; 060import ptolemy.util.MessageHandler; 061 062/////////////////////////////////////////////////////////////////// 063//// HTMLViewer 064 065/** 066 This class is a toplevel frame that can view HTML documents. 067 This class supports hyperlinks, and has a particular feature to 068 force hyperlinks to be opened in a browser. To do that, specify 069 a hyperlink by giving a fragment (also called a reference) as "in_browser". 070 For example, the following URL will be opened in a browser: 071 <pre> 072 <a href="http://ptolemy.eecs.berkeley.edu#in_browser"> 073 </pre> 074 075 If the URL is <code>about:copyright</code>, then the copyrights will 076 be generated by {@link ptolemy.actor.gui.GenerateCopyrights#generateHTML(Configuration)} 077 078 <p>If the URL is <code>about:configuration</code>, then the 079 Ptolemy II configuration will be expanded by and the MoML of the 080 configuration will be returned. This is a good way to test the 081 configuration. 082 083 <p>If the URL starts with <code>ptdoc:</code>, then the Ptolemy 084 documentation is opened. For example 085 <pre> 086 < a href="ptdoc:ptolemy.actor.gui.HTMLViewer">HTMLViewer</a> 087 </pre> 088 will open the Ptolemy documentation for this class. For details see 089 {@link ptolemy.vergil.basic.GetDocumentationAction}. 090 091 <p>If the URL starts with <code>$CLASSPATH</code> then the classpath 092 is searched.</p> 093 094 <p>This class supports printing and will save the text to a .html file. 095 The url that is viewed can be changed by calling the <i>setPage</i> method. 096 097 @author Steve Neuendorffer and Edward A. Lee 098 @version $Id$ 099 @since Ptolemy II 1.0 100 @Pt.ProposedRating Yellow (eal) 101 @Pt.AcceptedRating Red (johnr) 102 */ 103@SuppressWarnings("serial") 104public class HTMLViewer extends TableauFrame 105 implements Printable, HyperlinkListener { 106 /** Construct a blank viewer. 107 */ 108 public HTMLViewer() { 109 _init(); 110 } 111 112 /** Construct an empty top-level frame managed by the specified 113 * tableau and the default status bar. After constructing this, 114 * it is necessary to call setVisible(true) to make the frame appear. 115 * It may also be desirable to call centerOnScreen(). 116 * @param tableau The managing tableau. 117 */ 118 public HTMLViewer(Tableau tableau) { 119 super(tableau); 120 _init(); 121 } 122 123 /////////////////////////////////////////////////////////////////// 124 //// public methods //// 125 126 /** Give a ptdoc: path, open the PtDoc viewer. 127 * @param configuration The Configuration. 128 * @param className The dot separated classname, such as 129 * ptolemy.kernel.util.NamedObj. 130 * @param context The controlling Effigy. 131 * @exception IllegalActionException If thrown while searching 132 * for the _getDocumentationActionClassName attribute in the 133 * Configuration. 134 * @exception ClassNotFoundException If the class named by the 135 * _getDocumentationActionClassName attribute or 136 * ptolemy.vergil.basic.GetDocumentationAction is not found. 137 * @exception NoSuchMethodException If the class does not have 138 * a getDocumentation(Configuration, String, Effigy) method. 139 * @exception IllegalAccessException If thrown while calling 140 * the getDocumentation() method. 141 * @exception InvocationTargetException If thrown while calling 142 * the getDocumentation() method. 143 */ 144 public static void getDocumentation(Configuration configuration, 145 String className, Effigy context) throws IllegalActionException, 146 ClassNotFoundException, NoSuchMethodException, 147 IllegalAccessException, InvocationTargetException { 148 // Read the _getDocumentationActionClassName from 149 // the configuration and attempt to call it. 150 // If _getDocumentationActionClassName is not set, 151 // then default to vergil GetDocumentationAction. 152 153 // FIXME: Refactor this code, use DocApplicationSpecializer 154 155 StringAttribute getDocumentationActionClassNameStringAttribute = (StringAttribute) configuration 156 .getAttribute("_getDocumentationActionClassName", 157 StringAttribute.class); 158 String getDocumentationActionClassName = null; 159 if (getDocumentationActionClassNameStringAttribute != null) { 160 getDocumentationActionClassName = getDocumentationActionClassNameStringAttribute 161 .getExpression(); 162 } else { 163 getDocumentationActionClassName = "ptolemy.vergil.basic.GetDocumentationAction"; 164 } 165 Class getDocumentationActionClass = Class 166 .forName(getDocumentationActionClassName); 167 Method getDocumentationMethod = getDocumentationActionClass 168 .getMethod("getDocumentation", new Class[] { 169 Configuration.class, String.class, Effigy.class }); 170 //GetDocumentationAction.getDocumentation(configuration, 171 // event.getDescription().substring(6), getEffigy()); 172 getDocumentationMethod.invoke(null, 173 new Object[] { configuration, className, context }); 174 } 175 176 /** Get the page displayed by this viewer. 177 * @return The page displayed by this viewer. 178 * @see #setPage(URL) 179 */ 180 public URL getPage() { 181 return pane.getPage(); 182 } 183 184 /** React to a hyperlink being clicked on in the rendered HTML. 185 * This method opens the hyperlink URL in a new window, using 186 * the configuration. This means that hyperlinks can reference 187 * any file that the configuration can open, including MoML files. 188 * It is assumed this is called in the AWT event thread. 189 * @param event The hyperlink event. 190 */ 191 @Override 192 public void hyperlinkUpdate(HyperlinkEvent event) { 193 if (event.getEventType() == HyperlinkEvent.EventType.ENTERED) { 194 if (event.getURL() != null) { 195 // If the link was 'about:copyright', 196 // then getURL() returns null, but getDescription() works. 197 report(event.getURL().toString()); 198 } else if (event.getDescription() != null) { 199 report(event.getDescription()); 200 } 201 } else if (event.getEventType() == HyperlinkEvent.EventType.EXITED) { 202 report(""); 203 } else if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { 204 URL newURL = event.getURL(); 205 206 if (event.getDescription().startsWith("about:")) { 207 // Process "about:" hyperlinks 208 try { 209 newURL = HTMLAbout.hyperlinkUpdate(event, 210 getConfiguration()); 211 212 } catch (Throwable throwable) { 213 MessageHandler.error("Problem processing '" 214 + event.getDescription() + "'.", throwable); 215 } 216 } 217 218 if (event.getDescription().startsWith("ptdoc:")) { 219 // Process "ptdoc:" hyperlinks 220 try { 221 getDocumentation(getConfiguration(), 222 event.getDescription().substring(6), getEffigy()); 223 } catch (Throwable throwable) { 224 MessageHandler.error("Problem processing '" 225 + event.getDescription() + "'.", throwable); 226 } 227 } 228 // NOTE: It would be nice to use target="_browser" or some 229 // such, but this doesn't work. Targets aren't 230 // seen unless the link is inside a frame, 231 // regrettably. An alternative might be to 232 // use the "userInfo" part of the URL, 233 // defined at http://www.ncsa.uiuc.edu/demoweb/url-primer.html 234 boolean useBrowser = false; 235 236 if (newURL != null) { 237 String ref = newURL.getRef(); 238 239 if (ref != null) { 240 useBrowser = ref.equals("in_browser"); 241 } 242 243 String protocol = newURL.getProtocol(); 244 245 if (protocol != null) { 246 // Suggested mailto: extension from Paul Lieverse 247 useBrowser |= protocol.equals("mailto"); 248 } 249 } else { 250 // The URL is null, for some reason. This could happen, 251 // for example, if the HTML to be displayed is specified 252 // using setText() instead of setPage(). In this case, 253 // if relative URLs are to be supported, it is up to the 254 // using class to call setBase() to specify the relative 255 // URL. 256 try { 257 newURL = new URL(_base, event.getDescription()); 258 } catch (MalformedURLException e) { 259 report("Link error: " + event.getDescription()); 260 return; 261 } 262 } 263 264 if (!useBrowser && event instanceof HTMLFrameHyperlinkEvent) { 265 // For some bizarre reason, when a link is within a frame, 266 // it needs to be handled differently than if its not in 267 // a frame. 268 HTMLFrameHyperlinkEvent frameHyperlinkEvent = (HTMLFrameHyperlinkEvent) event; 269 String target = frameHyperlinkEvent.getTarget(); 270 271 if (target.equals("_browser")) { 272 useBrowser = true; 273 } else if (!target.equals("_blank") && !target.equals("_top")) { 274 // If the target is "_blank" or "_top", then we want to open 275 // in a new window, so we defer to the below. 276 HTMLDocument doc = (HTMLDocument) pane.getDocument(); 277 try { 278 doc.processHTMLFrameHyperlinkEvent(frameHyperlinkEvent); 279 } catch (Exception ex) { 280 MessageHandler.error("Hyperlink reference failed", ex); 281 } 282 283 return; 284 } 285 } 286 287 try { 288 // If the URL is the same as the one we are currently in, 289 // then we are dealing with a link within the same file, 290 // so we want to stay in the same window. 291 if (getPage() != null 292 && newURL.getFile().equals(getPage().getFile())) { 293 pane.setPage(newURL); 294 } else { 295 // Attempt to open in a new window. 296 Configuration configuration = getConfiguration(); 297 298 // FIXME: Should detect target == "_blank" and open 299 // in a new window, rather than always opening in a new 300 // window. However, regrettably, there appears to be 301 // no way to access the target unless the event is an 302 // instanceof HTMLFrameHyperlinkEvent, which it is only 303 // if the HTML happens to be in a frame. Moreover, it would 304 // be tricky to do this because we would have to check that 305 // the content type is "text/html" or "text/rtf", and we 306 // would have to associate our tableau with a new effigy. 307 // Nonetheless, it's perfectly doable if we can get the 308 // target... 309 if (configuration != null) { 310 if (useBrowser && BrowserEffigy.staticFactory != null) { 311 // Note that openModel will call MessageHandler 312 // if there are problems, so there is no point 313 // putting a try/catch block here. 314 configuration.openModel(newURL, newURL, 315 newURL.toExternalForm(), 316 BrowserEffigy.staticFactory); 317 } else { 318 try { 319 configuration.openModel(newURL, newURL, 320 newURL.toExternalForm()); 321 } catch (IOException ex) { 322 // Try searching in the classpath in case the event description 323 // starts with $CLASSPATH. 324 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5194 325 URL eventURL = null; 326 String eventDescription = event 327 .getDescription(); 328 try { 329 eventURL = FileUtilities.nameToURL( 330 eventDescription, null, null); 331 if (eventURL == null) { 332 throw new NullPointerException( 333 "Could not find \"" 334 + eventDescription 335 + "\""); 336 } 337 configuration.openModel(eventURL, eventURL, 338 eventURL.toExternalForm()); 339 } catch (Throwable throwable) { 340 if (eventDescription.indexOf(":/") == -1 341 || eventDescription 342 .startsWith("/")) { 343 URL eventURL2 = null; 344 try { 345 // Try in the $CLASSPATH. 346 // One test is to view the docs in PNDirector from the website 347 // (no local docs) and then try to follow the links to other models. 348 if (eventDescription 349 .startsWith("/")) { 350 eventDescription = eventDescription 351 .substring(1); 352 } 353 String classpathEventDescription = "$CLASSPATH/" 354 + eventDescription; 355 eventURL2 = FileUtilities.nameToURL( 356 classpathEventDescription, 357 null, null); 358 if (eventURL2 == null) { 359 throw new NullPointerException( 360 "Could not find \"" 361 + classpathEventDescription 362 + "\""); 363 } 364 configuration.openModel(eventURL2, 365 eventURL2, 366 eventURL2.toExternalForm()); 367 } catch (Throwable throwable2) { 368 IOException exception = new IOException( 369 "Failed to find " + newURL 370 + ", also tried\n " 371 + eventURL 372 + " and\n" 373 + eventURL2); 374 exception.initCause(ex); 375 throw exception; 376 } 377 } 378 } 379 } 380 } 381 } else { 382 // If there is no configuration, 383 // open in the same window. 384 pane.setPage(newURL); 385 } 386 } 387 } catch (Exception ex) { 388 MessageHandler.error("Hyperlink reference failed", ex); 389 } 390 } 391 } 392 393 // FIXME: This should be handled in Top... 394 395 /** Print the documentation to a printer. The documentation will be 396 * scaled to fit the width of the paper, growing to as many pages as 397 * is necessary. 398 * @param graphics The context into which the page is drawn. 399 * @param format The size and orientation of the page being drawn. 400 * @param index The zero based index of the page to be drawn. 401 * @return PAGE_EXISTS if the page is rendered successfully, or 402 * NO_SUCH_PAGE if pageIndex specifies a non-existent page. 403 * @exception PrinterException If the print job is terminated. 404 */ 405 @Override 406 public int print(Graphics graphics, PageFormat format, int index) 407 throws PrinterException { 408 Dimension dimension = pane.getSize(); 409 410 // How much do we have to scale the width? 411 double scale = format.getImageableWidth() / dimension.getWidth(); 412 double scaledHeight = dimension.getHeight() * scale; 413 int lastPage = (int) (scaledHeight / format.getImageableHeight()); 414 415 // If we're off the end, then we're done. 416 if (index > lastPage) { 417 return Printable.NO_SUCH_PAGE; 418 } 419 420 AffineTransform at = new AffineTransform(); 421 at.translate((int) format.getImageableX(), 422 (int) format.getImageableY()); 423 at.translate(0, -(format.getImageableHeight() * index)); 424 at.scale(scale, scale); 425 426 ((Graphics2D) graphics).transform(at); 427 428 pane.paint(graphics); 429 return Printable.PAGE_EXISTS; 430 } 431 432 /** Set the base URL for relative accesses. 433 * @param base The base for relative hyperlink references. 434 */ 435 public void setBase(URL base) { 436 _base = base; 437 } 438 439 /** Set the page displayed by this viewer to be that given by the 440 * specified URL. 441 * @param page The location of the documentation. 442 * @exception IOException If the page cannot be read. 443 * @see #getPage() 444 */ 445 public void setPage(URL page) throws IOException { 446 URL jarURL = ClassUtilities.jarURLEntryResource(page.toString()); 447 if (jarURL != null) { 448 // Under Java 1.7, JEditorPane.setPage() handles jar urls 449 // differently. In Java 1.6, setPage() would correctly open 450 // jar:file:/Users/cxh/ptII/ptolemy/ptsupport.jar!/doc/mainVergilPtiny.htm 451 // even though doc/MainVergilPtiny.htm is in doc/docConfig.jar, 452 // not ptsupport.jar. So, we look up the jar URL. 453 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5508 454 page = jarURL; 455 } 456 pane.setPage(page); 457 } 458 459 /** Override the base class to set the size of the scroll pane. 460 * Regrettably, this is necessary because swing packers ignore 461 * the specified size of a container. If this is not called in 462 * the AWT event thread, then execution is deferred and executed 463 * in that thread. 464 * @param width The width of the scroll pane. 465 * @param height The height of the scroll pane. 466 */ 467 @Override 468 public void setSize(final int width, final int height) { 469 Runnable doSet = new Runnable() { 470 @Override 471 public void run() { 472 _setScrollerSize(width, height); 473 HTMLViewer.super.setSize(width, height); 474 } 475 }; 476 477 Top.deferIfNecessary(doSet); 478 } 479 480 /** Set the text displayed by this viewer. 481 * @param text The text to display. 482 */ 483 public void setText(String text) { 484 pane.setText(text); 485 } 486 487 /////////////////////////////////////////////////////////////////// 488 //// public variables //// 489 490 /** The text pane. */ 491 public JEditorPane pane = new JEditorPane(); 492 493 /////////////////////////////////////////////////////////////////// 494 //// protected methods //// 495 496 /** Add the main content pane (for HTML). 497 */ 498 protected void _addMainPane() { 499 // Default, which can be overridden by calling setSize(). 500 _scroller.setPreferredSize(new Dimension(800, 600)); 501 getContentPane().add(_scroller); 502 } 503 504 /** Set the scroller size. 505 * @param width The width. 506 * @param height The width. 507 */ 508 protected void _setScrollerSize(final int width, final int height) { 509 _scroller.setPreferredSize(new Dimension(width, height)); 510 _scroller.setSize(new Dimension(width, height)); 511 } 512 513 /** Write the model to the specified file. Note that this does not 514 * defer to the effigy. 515 * @param file The file to write to. 516 * @exception IOException If the write fails. 517 */ 518 @Override 519 protected void _writeFile(File file) throws IOException { 520 java.io.FileWriter fileWriter = null; 521 522 try { 523 fileWriter = new java.io.FileWriter(file); 524 fileWriter.write(pane.getText()); 525 } finally { 526 if (fileWriter != null) { 527 fileWriter.close(); 528 } 529 } 530 } 531 532 /////////////////////////////////////////////////////////////////// 533 //// protected variables //// 534 535 /** The main scroll pane. */ 536 protected JScrollPane _scroller; 537 538 /////////////////////////////////////////////////////////////////// 539 //// private methods //// 540 541 /** Initialize the HTMLViewer. 542 */ 543 private void _init() { 544 getContentPane() 545 .setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); 546 pane.setContentType("text/html"); 547 pane.setEditable(false); 548 pane.addHyperlinkListener(this); 549 550 if (_styleSheetURL != null) { 551 // If _styleSheetURL is non-null, we set the style sheet 552 // once and only once. If try to do this in a static initializer, 553 // then the styles are wrong. 554 HTMLDocument doc = (HTMLDocument) pane.getDocument(); 555 StyleSheet styleSheet = doc.getStyleSheet(); 556 styleSheet.importStyleSheet(_styleSheetURL); 557 if (_HTMLEditorKit == null) { 558 _HTMLEditorKit = new HTMLEditorKit(); 559 } 560 _HTMLEditorKit.setStyleSheet(styleSheet); 561 _styleSheetURL = null; 562 } 563 564 // http://mindprod.com/jgloss/antialiasing.html says that in 565 // java 1.5, this will turn on anti-aliased fonts 566 try { 567 // We use reflection so that this compiles everywhere. 568 Class swingUtilities = Class 569 .forName("com.sun.java.swing.SwingUtilities2"); 570 Field propertyField = swingUtilities 571 .getDeclaredField("AA_TEXT_PROPERTY_KEY"); 572 pane.putClientProperty(propertyField.get(null), Boolean.TRUE); 573 } catch (Throwable ex) { 574 // Ignore, we just wont have anti-aliased fonts then. 575 } 576 577 _scroller = new JScrollPane(pane); 578 _addMainPane(); 579 } 580 581 /////////////////////////////////////////////////////////////////// 582 //// private variables //// 583 584 /** The base as specified by setBase(). */ 585 private URL _base; 586 587 /** The HTMLEditorKit associated with this viewer. */ 588 private static HTMLEditorKit _HTMLEditorKit; 589 590 /** The url that refers to $PTII/doc/default.css. */ 591 private static URL _styleSheetURL; 592 593 static { 594 try { 595 Class refClass = Class.forName("ptolemy.kernel.util.NamedObj"); 596 _styleSheetURL = refClass.getClassLoader() 597 .getResource("doc/default.css"); 598 } catch (Throwable ex) { 599 ex.printStackTrace(); 600 // Ignore, we just use the wrong style sheets. 601 } 602 603 } 604 605 // static { 606 // try { 607 // // We might be in the Swing Event thread, so 608 // // Thread.currentThread().getContextClassLoader() 609 // // .getResource(entry) probably will not work. 610 // Class refClass = Class.forName("ptolemy.kernel.util.NamedObj"); 611 // URL styleSheetURL = refClass.getClassLoader() 612 // .getResource("doc/default.css"); 613 // if (styleSheetURL != null) { 614 // System.out.println("HTMLViewer: reading stylesheet " 615 // + styleSheetURL + "Instead of " + HTMLEditorKit.DEFAULT_CSS); 616 617 // StyleSheet styleSheet = htmlEditorKit.getStyleSheet(); 618 // styleSheet.importStyleSheet(styleSheetURL); 619 // htmlEditorKit.setStyleSheet(styleSheet); 620 // } else { 621 // System.out.println("Failed to read doc/default.css, so " 622 // + " the wrong style sheets will be used."); 623 // } 624 // } catch (Throwable ex) { 625 // // Ignore, we just use the wrong style sheets. 626 // ex.printStackTrace(); 627 // } 628 // } 629}