The javax.swing.JEditorPaneclass and its HTML editor kit make it easy to present HTML documents. Because this editor kit’s HTML support is limited (Java applets and
JavaScript are not supported, for example), JEditorPaneis not appropriate for implement- ing a web browser that can browse arbitrary web sites. However, this class is ideal for integrating web-based online help into Java applications (although you might prefer to work with the JavaHelp API).
■ Note Despite JEditorPanebeing inappropriate for implementing a generalized web browser, I used this class as the basis for two such web browser applications in Chapter 4 (Listings 4-1 and 4-7), for conven- ience. These applications demonstrated Java SE 6’s “place arbitrary components on a tabbed pane’s tab headers” and “print text component” features.
In an online-help scenario, an application’s help documentation consists of web pages stored on a specific web site. It is easier to maintain help documentation in a single location than to update the documentation in many places. Because the application’s editor pane is restricted to this web site, the pages’ HTML can be limited to the features with which the editor pane can work.
The absence of JavaScript support makes it difficult to give the web pages a dynamic quality; for example, to change a link’s color to some other color when the mouse pointer hovers over the link. Fortunately, it is possible to integrate JavaScript into the editor pane via the Scripting API and some editor pane knowledge.
To prove my point, I have developed a ScriptedEditorPaneclass that extends JEditorPaneand evaluates a web page’s JavaScript code via the Rhino script engine.
I’ve also created an application that embeds the scripted editor pane component into its GUI, to demonstrate this component. Listing 9-12 presents this application.
Listing 9-12.DemoScriptedEditorPane.java
// DemoScriptedEditorPane.java import java.awt.*;
import java.io.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
public class DemoScriptedEditorPane extends JFrame implements HyperlinkListener
{
private JLabel lblStatus;
DemoScriptedEditorPane () {
super ("Demo ScriptedEditorPane");
setDefaultCloseOperation (EXIT_ON_CLOSE);
ScriptedEditorPane pane = null;
try {
// Create a scripted editor pane component that loads the contents // of a test.html file, which is located in the current directory.
pane = new ScriptedEditorPane ("file:///"+
new File ("").getAbsolutePath ()+
"/demo.html");
pane.setEditable (false);
pane.setBorder (BorderFactory.createEtchedBorder ());
pane.addHyperlinkListener (this);
}
catch (Exception e) {
System.out.println (e.getMessage ());
return;
}
getContentPane ().add (pane, BorderLayout.CENTER);
lblStatus = new JLabel (" ");
lblStatus.setBorder (BorderFactory.createEtchedBorder ());
getContentPane ().add (lblStatus, BorderLayout.SOUTH);
setSize (350, 250);
setVisible (true);
}
public void hyperlinkUpdate (HyperlinkEvent hle) {
HyperlinkEvent.EventType evtype = hle.getEventType ();
if (evtype == HyperlinkEvent.EventType.ENTERED) lblStatus.setText (hle.getURL ().toString ());
else
if (evtype == HyperlinkEvent.EventType.EXITED) lblStatus.setText (" ");
}
public static void main (String [] args) {
Runnable r = new Runnable () {
public void run () {
new DemoScriptedEditorPane ();
} };
EventQueue.invokeLater (r);
} }
The application’s Swing GUI consists of a scripted editor pane and a status bar label.
The editor pane displays the contents of a demo.htmlfile, which must be located in the current directory. The status bar presents the URL that is associated with the link over which the mouse pointer is hovering. Move the mouse pointer over a link to change the link’s color. Figure 9-2 shows this GUI.
Figure 9-2.The scripted editor pane integrates JavaScript via the Scripting API.
The demo.htmlfile, shown in Listing 9-13, describes an HTML document that defines two JavaScript functions between one pair of <script>and </script>tags. (It is possible to specify multiple pairs of <script>and </script>tags.) It also specifies onmouseoverand onmouseoutattributes for each of its two anchor tags (</a>). Each attribute’s JavaScript code invokes one of the defined functions.
Listing 9-13.demo.html
<html>
<head>
<script>
function setColor(color) {
document.linkcolor = color;
println (document.linkcolor);
}
function revertToDefaultColor() {
document.linkcolor = document.defaultlinkcolor;
}
</script>
</head>
<body>
<h1>demo.html</h1>
Demonstrate JavaScript logic for changing link colors.
<p>
<a href="first.html" onmouseover="setColor (java.awt.Color.red);"
onmouseout="setColor (java.awt.Color.magenta);">
first link</a>
<p>
<a href="second.html" onmouseover="setColor (java.awt.Color.green);"
onmouseout="revertToDefaultColor();">
second link</a>
<!-- I chose first.html and second.html to serve as example href values.
The actual files do not exist; they are not needed. -->
</body>
</html>
Listing 9-13 refers to a documentobject that is associated with the currently displayed HTML document. This object defines only linkcolorand defaultlinkcolorproperties, whose values are java.awt.Colorinstances. The linkcolorproperty describes the color of the link being made active or inactive; it can be set or read. defaultlinkcoloris a read-only property that specifies the default color for all links.
Now that you are familiar with DemoScriptedEditorPaneand demo.html, it should be somewhat easier to understand ScriptedEditorPane’s implementation. This implementa- tion consists of five private instance fields, two public constructors, one public method, and three private inner classes. Listing 9-14 shows the ScriptedEditorPanesource code.
Listing 9-14.ScriptedEditorPane.java
// ScriptedEditorPane.java import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.script.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.text.html.parser.*;
public class ScriptedEditorPane extends JEditorPane {
// The anchor element associated with the most recent hyperlink event. It // probably should be located in the ScriptEnvironment, where it is used.
private javax.swing.text.Element currentAnchor;
// The Rhino script engine.
private ScriptEngine engine;
// The Java environment corresponding to the JavaScript document object.
private ScriptEnvironment env;
// An initialization script that connects a JavaScript document object with // linkcolor and defaultlinkcolor properties, to an adapter with __get__() // and __put()__ member functions, which access the script environment.
private String initScript =
"var document = new JSAdapter ({"+
" __get__ : function (name)"+
" {"+
" if (name == 'defaultlinkcolor')"+
" return env.getDefaultLinkColor ();"+
" else"+
" if (name == 'linkcolor')"+
" return env.getLinkColor ();"+
" },"+
" __put__ : function (name, value)"+
" {"+
" if (name == 'linkcolor')"+
" env.setLinkColor (value);"+
" }"+
"})";
// The concatenated contents of all <script></script> sections in top-down // order.
private String script;
// Create a scripted editor pane without an HTML document. A document can // be subsequently added via a setPage() call.
public ScriptedEditorPane () throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager ();
engine = manager.getEngineByName ("rhino");
// For convenience, I throw a ScriptException instead of creating a new // exception class for this purpose.
if (engine == null)
throw new ScriptException ("no Rhino script engine");
// Set up environment for JSAdapter and evaluate initialization script.
env = new ScriptEnvironment ();
engine.put ("env", env);
engine.eval (initScript);
addHyperlinkListener (new ScriptedLinkListener ());
}
// Create a scripted editor pane with the specified HTML document.
public ScriptedEditorPane (String pageUrl) throws IOException, ScriptException {
this ();
setPage (pageUrl);
}
// Associate an HTML document with the scripted editor pane. Prior to the // association, the document is parsed to extract the contents of all
// <script></script sections.
public void setPage (URL url) throws IOException {
InputStreamReader isr = new InputStreamReader (url.openStream ());
BufferedReader reader;
reader = new BufferedReader (isr);
Callback cb = new Callback ();
new ParserDelegator ().parse (reader, cb, true);
reader.close ();
script = cb.getScript ();
super.setPage (url);
}
// Extract the contents of all <script> sections via this callback. Because // the parser exposes these contents as if they were HTML comments, care is // needed to differentiate them from actual HTML comments. Learn more about // the parser from Jeff Heaton's "Parsing HTML with Swing" article
// (http://www.samspublishing.com/articles/article.asp?p=31059&seqNum=1).
private class Callback extends HTMLEditorKit.ParserCallback {
// A <script></script> section is being processed when this variable is // true. It defaults to false.
private boolean inScript;
// The contents of all <script></script> sections are stored in a // StringBuffer instead of a String to minimize String object creation.
private StringBuffer scriptBuffer = new StringBuffer ();
// Return the script.
String getScript () {
return scriptBuffer.toString ();
}
// Only append the data to the string buffer if the parser has already // detected a <script> tag.
public void handleComment (char [] data, int pos) {
if (inScript)
scriptBuffer.append (data);
}
// Detect a <script> tag.
public void handleStartTag (HTML.Tag t,
MutableAttributeSet a, int pos) {
if (t == HTML.Tag.SCRIPT) inScript = true;
}
// Detect a </script> tag.
public void handleEndTag (HTML.Tag t, int pos) {
if (t == HTML.Tag.SCRIPT) inScript = false;
} }
// Provide the glue between document's properties and the Java environment // in which the script runs.
private class ScriptEnvironment {
// The default color of an anchor tag's link text as determined by the // current CSS style sheet.
private Color defaultLinkColor;
// Create a script environment. Extract the default link color via the // current CSS style sheet.
ScriptEnvironment () {
HTMLEditorKit kit;
kit = (HTMLEditorKit) getEditorKitForContentType ("text/html");
StyleSheet ss = kit.getStyleSheet ();
Style style = ss.getRule ("a"); // Get rule for anchor tag.
if (style != null) {
Object o = style.getAttribute (CSS.Attribute.COLOR);
defaultLinkColor = ss.stringToColor (o.toString ());
} }
// Return the default link color.
public Color getDefaultLinkColor () {
return defaultLinkColor;
}
// Return the link color of the current anchor element.
public Color getLinkColor () {
AttributeSet as = currentAnchor.getAttributes ();
return StyleConstants.getForeground (as);
}
// Set the link color for the current anchor element.
public void setLinkColor (Color color) {
StyleContext sc = StyleContext.getDefaultStyleContext ();
AttributeSet as = sc.addAttribute (SimpleAttributeSet.EMPTY, StyleConstants.Foreground, color);
((HTMLDocument) currentAnchor.getDocument ()).
setCharacterAttributes (currentAnchor.getStartOffset (), currentAnchor.getEndOffset ()- currentAnchor.getStartOffset(), as, false);
} }
// Provide a listener for identifying the current anchor element, detecting // an onmouseover attribute (for an entered event) or an onmouseout element // (for an exited event) that is associated with this element's <a> tag, // and evaluating this attribute's JavaScript code.
private class ScriptedLinkListener implements HyperlinkListener {
// For convenience, this listener's hyperlinkUpdate() method ignores // HTML frames.
public void hyperlinkUpdate (HyperlinkEvent he) {
HyperlinkEvent.EventType type = he.getEventType ();
if (type == HyperlinkEvent.EventType.ENTERED) {
currentAnchor = he.getSourceElement ();
AttributeSet as = currentAnchor.getAttributes ();
AttributeSet asa = (AttributeSet) as.getAttribute (HTML.Tag.A);
if (asa != null) {
Enumeration<?> ean = asa.getAttributeNames ();
while (ean.hasMoreElements ()) {
Object o = ean.nextElement ();
if (o instanceof String) {
String attr = o.toString ();
if (attr.equalsIgnoreCase ("onmouseover")) {
String value = (String) asa.getAttribute (o);
try {
engine.eval (script+value);
}
catch (ScriptException se) {
System.out.println (se);
} break;
} } } } } else
if (type == HyperlinkEvent.EventType.EXITED) {
currentAnchor = he.getSourceElement ();
AttributeSet as = currentAnchor.getAttributes ();
AttributeSet asa = (AttributeSet) as.getAttribute (HTML.Tag.A);
if (asa != null) {
Enumeration<?> ean = asa.getAttributeNames ();
while (ean.hasMoreElements ()) {
Object o = ean.nextElement ();
if (o instanceof String) {
String attr = o.toString ();
if (attr.equalsIgnoreCase ("onmouseout")) {
String value = (String) asa.getAttribute (o);
try {
engine.eval (script+value);
}
catch (ScriptException se) {
System.out.println (se);
} break;
} } } } } } } }
Despite its many comments, you will probably have a number of questions as you study Listing 9-14. The following points should answer at least some of those questions:
• I use JSAdapter(in an initially evaluated script) to connect the documentobject’s linkcolorand defaultlinkcolorproperties to a delegate’s member function calls. It seems more natural to access documentproperties than to invoke documentmember functions. The __get()__member function translates property reads into calls to ScriptEnvironment’s public Color getLinkColor()and Color getDefaultLinkColor() methods. The __put()__member function translates a property write on linkcolor into a call to the equivalent public void setLinkColor(Color color)method.
• I deliberately limit ScriptedEditorPane’s document object model to ScriptEnvironment (perhaps I should have named this class ScriptDOM) and JSAdapter. Creating a sophisticated document object model is not a trivial undertaking. Among various considerations, you need to decide if this model should be external to the scripted editor pane component. This decision will impact how you access a status bar component from the model, for example.
• I override JEditorPane’s public void setPage(URL url)method so that I can extract the content of each encountered <script>and </script>tag pair during an initial parsing operation. I cannot extract this content by depending on the HTML editor kit’s internal parsing. Perhaps there is a way to extract this content and avoid the problem of parsing the url’s content twice, but I have yet to find it.
• For simplicity, I do not work with the HTMLEditorKit.LinkControllerclass, which can be used in situations where the mouse pointer hovers over an arbitrary HTML element (such as an image not associated with a link). Providing the custom editor kit necessary to work with LinkControllerwould have added complexity to an oth- erwise simple example. In contrast, ScriptedLinkListeneraddresses only the limited scenarios of entering or exiting (or, if modified, activating) a link.
If you plan to modify the scripted editor pane component, or if you just want to gain a deeper understanding of the code within ScriptedLinkListener’s public void hyperlinkUpdate(HyperlinkEvent he)method (not to mention the code within the ScriptEnvironmentinner class), you will benefit from a book that extensively covers Swing’s text components. One book that I have found to be very helpful in this regard is Java Swing, Second Edition by Marc Loy, Robert Eckstein, Dave Wood, James Elliott, and Brian Cole (O’Reilly, 2002).
■ Tip The authors of Java Swing, Second Edition created two sample PDF-based chapters on the HTML editor kit and HTML I/O, which can be downloaded from http://examples.oreilly.com/jswing2/
code/goodies/misc.html.