// (c) 2003 Allen I Holub. All rights reserved.
package com.holub.ui.HTML;
import com.hexidec.ekit.component.ExtendedHTMLDocument;
import com.hexidec.ekit.component.RelativeImageView;
import com.holub.net.UrlUtil;
import com.holub.tools.Log;
import com.holub.ui.AncestorAdapter;
import com.holub.ui.HTML.TagBehavior;
import com.holub.ui.HTML.FilterFactory;
import java.io.*;
import java.util.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import java.util.logging.*;
import java.util.regex.*;
import javax.swing.*;
import javax.swing.text.*; // View Element ViewFactory
import javax.swing.text.html.*;
import javax.swing.event.*;
import javax.swing.border.*;
// See full documentation for this class in last-month's article
/*** ****************************************************************
* This class let's you build an HTML-based interface with JSP-like
* functionality that operates entirely within the confines of a
* client-side program---no web server is necessary.
* <code>HTMLPane</code> also fixes several problems in {@link JEditorPane}'s
* HTML support, but it is based on {@link JEditorPane}, so has many of its
* limitations.
* <p>
* <code>HTMLPane</code> augments {@link JEditorPane} in several ways:
* <ul>
* <li>An <code>HTMLPane</code> logs errors to the com.holub.ui logger
* rather than throwing exceptions in many situations. You can hook
* into these messages by creating a logger (described below).
* <li>I've improved the appearance of form elements a bit:
* <ul>
* <li>Radio buttons and text-input fields are now aligned properly
* with the surrounding text
* <li>The list created by a <code><select></code>
* element is now only a little wider than the contained text
* (instead of taking up the full screen).
* <li>Radio buttons and check boxes have transparent backgrounds so that
* they don't look weird against background textures and
* colors that aren't dark grey (<code>bgcolor="d0d0d0"</code>).
* </ul>
* <li>You can add {@linkplain #addTag custom tags} and handlers for them,
* so you add JSP-style behavior to your forms.
* <li>You can specify a local form-data handler that's invoked in
* response to both submit-style and cancel-style operations. That is,
* clicking a "Submit" button causes a chuck of code (that you provide)
* to execute rather than sending the form data to a remote server.
* <li>
* HTTP hyperlinks are supported, but note that the JDK 1.4 runtime
* does not support the format: "file://c:/file."
* You have to use "file:/c:/file" (one slash).
* Strictly speaking, the 1.4 behavior is actually "correct," but some
* the rules for forming a "correct" URL are not well known, and the
* lack of a support for a double slash might be frustrating.
* <li>
* Mailto: hyperlinks are supported, but only on the Windows platform.
* Subject lines have to be properly URL encoded, (with %20 used for spaces,
* etc.). For example:
* <PRE>
* <a href="mailto:allen@holub.com?subject=foo%20bar">
* </PRE>
* Attempts to use mailto on other platforms result in a
* logged warning, but are otherwise silently absorbed.
* <li>
* The <code><a target=_blank ...></code> attribute now correctly
* causes the page to pop up in its own window. The window is a
* <code>pack()</code>ed frame that holds an HTMLPane, so your custom-tag
* handlers will work in the popup.
* <li>
* Standard http: and file: hyperlinks and relative URLs are handled
* internally, but only those hyperlinks that reference files that end
* in certain {@linkplain #redirect(URL) file
* extensions} are processed.
* Other protocols (e.g. ftp:) are not supported.
* <li>
* <code>HTMLPane</code> supports a
* {@linkplain #addHostMapping(String,String) host-mapping feature}
* that let's you specify maps between the "host" part of a URL and an
* arbitrary map-to URL. All hyperlinks aimed at the host will end up
* at the map-to location. This way you can map network URLs to
* file:// URLs for testing or non-network use.
* </ul>
* <p>
* <font size=+1><b>Logging</b></font>
* </p>
* The <code>HTMLPanel</code> logs errors and warnings to the
* com.holub.ui logger.
* To view the messages from all programs that use <code>HTMLPanel</code>,
* modify the .../jre/lib/logging.properties file,
* setting the <i>.level</i> and
* <i>java.util.logging.ConsoleHandler.level</i>
* properties to <i>ALL</i>. You can also
* put the following code into your program to turn on logging for
* that program only:
* <PRE>
* static
* { Logger log = Logger.getLogger("com.holub.ui");
* Handler h = new ConsoleHandler();
* h. setLevel(Level.ALL);
* log.setLevel(Level.ALL);
* log.addHandler(h);
* }
* </PRE>
* You can turn logging off by specifying Level.OFF instead of ALL.
* The {@link com.holub.tools.Log} class provides convenience
* methods for controlling logging.
* <p>
* <a name="customTags">
* <font size=+1><b>Custom Tags</b></font>
* </a>
* <p>
* You can define custom tags that can be used in your .html input,
* in a manner similar to a custom JSP tag. Set up a new tab by calling
* {@link #addTag addTag(...)}, passing it
* an object that identifies a {@linkplain TagHandler tag handler}
* that's activated when the tag is encountered.
* (See {@link #addTag addTag(...)} for more information.)
* <p>
* Because java's {@link
* javax.swing.JEditorPane}
* underlies the current class, you can't define a namespace-style
* custom tag: a name like
* <code><holub:myTag ...></code> (that contains a colon)
* isn't recognized. You can use underscores, but that's a kludge.
* <p>
* Also, the current implementation of custom tags does not permit the
* element to have content. The problem is that the default parser
* does not pair non-HTML tags. That is, <code><foo></code>
* and <code></foo></code> are treated as completely
* independent tags with no connection between them. The only
* solution is to effectively rewrite the parser, but that's
* a lot of work for not much gain. Future versions of this
* class might permit content, however.
* <p> Several pre-built custom tags are provided. Add support for
* these by issuing one of the following calls to you <code>HTMLPane</code>:
* <PRE>
* pane.{@link #addTag addTag}( "size" , new {@link SizeHandler SizeHandler}() );
* pane.{@link #addTag addTag}( "inputAction", new {@link InputActionHandler InputActionHandler}(this));
* pane.{@link #addTag addTag}( "inputNumber", new {@link InputNumberHandler InputNumberHandler}() );
* pane.{@link #addTag addTag}( "inputDate" , new {@link InputDateHandler InputDateHandler}() );
* </PRE>
* (These tags are all installed for you if you use the
* {@link #HTMLPane(boolean)} constructor).
*
* <PRE>
<size height=400 width=400>
* </PRE>
* <p style="padding-left:2em">
* Specifies the size of the pane in pixels.
* </p>
* <PRE>
* <a name="inputAction">
<inputAction name="myName" value="text">
* </PRE>
* <p style="padding-left:2em">
* This tag inserts a submit-style button that causes actionPerformed
* messages to be sent to all registered action listeners (as if the
* submit button had been pressed). The method and action attributes
* of the passed FormActionEvent will be empty (not null) Strings, and the
* data Properties object holds the pair <code>name=<em>XXX</em></code>,
* where <em>name</em> was specified using the <em>name=xxx</em> attribute in
* the original tag, and <em>XXX</em> holds the value <em>true</em> if the button
* was pressed, <em>false</em> if it wasn't.
* Key=value pairs supplied by custom
* tag handlers [added using {@link #addTag addTag(...)}] are also available, but
* the data supplied from standard HTML tags
* (<em><input></em>, <em><textarea></em>, and <em><select></em>)
* <span style="text-decoration:underline;">is not available</span>.
* The value attribute also defines the text
* on the button face. This tag is here to make
* it easy to implement a "cancel" or "quit" operation,
* but you can actually use it as a generic button if don't need the
* form data from the
* <em><input></em>, <em><textarea></em>, or <em><select></em>
* tags.
* </p>
* </a>
* <PRE>
<inputNumber name="fred" value="0.0" min="0" max="100" precision="2" size=<em>n</em>>
* </PRE>
* <p style="padding-left:2em">
* Like an <input type=text> tag, but inputs a numeric value
* in the range <em>min</em> ≤ <em>N</em> ≤ <em>max</em>, with up to <em>precision</em> digits
* permitted to the right of the decimal point (<em>precision can be zero</em>).
* The optional <em>size</em> attribute is the width of the field in columns.
* </p>
* <PRE>
<inputDate name="fred" value="10/15/03" size=<em>n</em>>
* </PRE>
* <p style="padding-left:2em">
* A localized date-input field that does data validation when it looses focus or you hit Enter.
* Most common date formats are recognized, and a popup dialog lets you choose dates from
* a calendar.
* The initial value, if present, must specify a date. (An empty string isn't permitted.)
* If no value= attribute is specified, today's date is used as the initial value.
* The optional <em>size</em> attribute is the width of the field in columns.
* (An approximation is used to size the control.)
* </p>
*
* <p>
* <font size=+1><b>Form Processing</b></font>
* <p>
* Normally, when an HTML form is submitted, the JEditorPane tries to
* actually execute and HTTP POST or GET operation on a remote server,
* passing it the data associated with the form elements as name=value
* pairs.
* <p>
* You can modify form-processing behavior, so that a form is submitted
* to the current program rather than to some server out on the net somewhere.
* Just add an {@link java.awt.event.ActionListener} to the HTMLPane by
* calling {@link #addActionListener addActionListener(...)}. The
* {@link java.awt.event.ActionListener#actionPerformed} method of the listener
* is called when the user hits the submit button. The associated
* <code>ActionEvent</code> object is actually an instance of the
* {@link FormActionEvent} class, and you can get the submit
* data from it.
* You may add as many action listeners as you like (they are all passed
* same event object). This way, the handlers can
* examine the <code>action=</code> attribute of the <code><form></code>
* tag and process data only if the associated URL is of interest.
* Once you've added any form handlers,
* <u>all</u> form submissions will go to them rather than being posted to the
* target URL. Your handler can relay the data to the web if it likes, but
* it must do so if you want that behavior.
* <p>
* Here's an example of a simple form-submission handler that just prints
* all the form-related information on standard output.
* <PRE>
* pane.addActionListener
* ( new ActionListener()
* { public void actionPerformed( ActionEvent event )
* {
* FormActionEvent act = (FormActionEvent)event;
*
* System.out.println("\n"+ act.getActionCommand() );
* System.out.println("\t"+"method=" + act.method() );
* System.out.println("\t"+"action=" + act.action() );
* act.data().list( System.out );
* System.out.println("");
* try
* { act.source().setPage // display a "success" page
* ( new URL(
* "file://c:/src/com/holub/ui/HTML/test/submit.html")
* );
* }
* catch( Exception e )
* { e.printStackTrace();
* }
* System.out.println("");
* }
* }
* );
* </PRE>
* <p>
* <font size=+1><b>Known Problems</b></font>
* <p>
* This class is based on {@link JEditorPane}, which does not
* use the world's best HTML parser. The following problems that are caused
* by Sun's parser aren't fixed in the current implementation:
* <ol>
* <li>The parser is dog slow.
* <li>CSS support is seriously broken. Styles are useless.
* <li>None of the new HTML 4 or XHTML tags are handled.
* <li>The parser doesn't handle tables very well.
* Very simple table nesting is okay, but complicated stuff fails
* miserably.
* <li><code><applet></code> tags are not supported---they are
* actually obsolescent as of HTML 4---but <code><object></code>
* tags are supported.
* Use the latter to embed your applets in a <code><form></code>.
* See <a href="http://java.sun.com/j2se/1.4/docs/guide/plugin/developerGuide/usingTags.html">
* the Java-plugin docs</a> for a discussion of the correct way to do this.
* <li>The JDK 1.4 parser parser does not handle
* <code><input type=submit name=x value=y></code>
* correctly (The problem is fixed in JDK ver. 1.5).
* In particular, the name=value string is not placed in the
* form data as it should be, so you cannot use multiple
* type=submit input fields in a single form.
* The new <button> and <input type=button> elements aren't
* supported either, so you can't use that tag.
* <p>
* I've added a <inputAction value="Text"&rt; tag for simple
* situations, but the implementation of the cancel tag does not
* give you access to the data that would com in with a normal
* submit operation.
* <li>The <code>JEditorPane</code> base class doesn't understand
* <code><script></code>
* tags (or JavaScript)---it just
* displays as normal text the contents of all
* <code><script></code> elements that are not nested inside
* comments.
* Unfortunately,
* javadoc-generated HTML doesn't follow the nest-script-in-comments
* convention, so you can't easily use <code>JEditorPane</code>
* as a Java-documentation browser.
* <li>The {@link JEditorPane} doesn't do frames correctly when you customize
* it (as I've done here).
* Frames appear to work, but pages displayed within
* frames are processed as if you were using a standard {@link
* javax.swing.JEditorPane}, with
* none of the features described here available to you. Consequently,
* you can't really use HTML frames. You can, however, create several
* HTMLPane instances and arrange them inside a JPanel (or other
* container) using a GridbagLayout or GridLayout.
* </ol>
* <p>
* I'm hoping that at least some {@link EditorKit} behavior
* will eventually get fixed. My guess is that the more fundamental
* structural problems (like the broken frame stuff) will probably
* stay broken, so the only long term solution is to toss the
* Sun implementation and replace it with something that works.
* <p>
* <DL>
* <DT><B>Requires:</B></DT>
* <DD>JDK1.4 (Regular expressions and
* <code>assert</code> statements are used.)
* </DD>
* </DL>
*
* <!-- ====================== distribution terms ===================== -->
* <p><blockquote
* style="border-style: solid; border-width:thin; padding: 1em 1em 1em 1em;">
* <center>
* Copyright © 2003, Allen I. Holub. All rights reserved.
* </center>
* <br>
* <br>
* This code is distributed under the terms of the
* <a href="http://www.gnu.org/licenses/gpl.html"
* >GNU Public License</a> (GPL)
* with the following ammendment to section 2.c:
* <p>
* As a requirement for distributing this code, your splash screen,
* about box, or equivalent must include an my name, copyright,
* <em>and URL</em>. An acceptable message would be:
* <center>
* This program contains Allen Holub's <em>XXX</em> utility.<br>
* (c) 2003 Allen I. Holub. All Rights Reserved.<br>
* http://www.holub.com<br>
* </center>
* If your progam does not run interactively, then the foregoing
* notice must appear in your documentation.
* </blockquote>
* <!-- =============================================================== -->
* @author Allen I. Holub
*/
//public class HTMLPane extends JEditorPane
// EkitCore needs to extend from JTextPane
// HTMLPane handles well customized tags
public class HTMLPane extends JTextPane
{
private FilterFactory filterProvider = FilterFactory.NULL_FACTORY;
private static final Logger log = Logger.getLogger("com.holub.ui");
/** A map of actual host names to replacement names, set by
* {@link #addHostMapping}
*/
private static Map hostMap = null;
/** Maps tags to Publishers of {@linkplain TagHandler handlers} for
* that tag
*/
private Map tagHandlers =
Collections.synchronizedMap(new HashMap());
/** A list of all components provided by a TagHandler that
* support the {@link TagBehavior} interface.
*/
private ActionListener actionListeners = null;
/** A list of all JComponents that act as stand in for custom
* tags that also implement TagBehavior.
*/
private Collection contributors = new LinkedList();
/**
* The name used as a key to get the tag-name attribute out of
* the attributes passed to a TagHandler object.
*/
public static final String TAG_NAME = "<tagName>";
/**
* All controls created from HTML tags (except for multi-line text input)
* are positioned so that they're aligned
* properly with respect to the text baseline. This way a radio button,
* for example, will line up with the text next to it. If you create
* a control of your own to be displayed in place of to a custom tag,
* you may want to issue a:
* <PRE>
* widget.setAlignmentY( BASELINE_ALIGNMENT );
* </PRE>
* request to get it aligned the same way as the standard controls
*/
public static final float BASELINE_ALIGNMENT = 0.70F;
//@constructor-start
/**
* Create an empty pane. Populate it by calling
* {@link JEditorPane#setPage(URL)} or {@link JEditorPane#setText(String)}
* <PRE>
* HTMLPane form = new HTMLPane();
* form.setPage( new URL("file://test.html") );
* </PRE>
* (For reasons that are not clear to me, the URL and String versions
* of the base-class constructors don't work when called from
* a derived-class constructor, so these base-class constructors
* are not exposed here.)
*/
public HTMLPane()
{
// modified
//registerEditorKitForContentType( "text/html",
// "com.holub.ui.HTMLPane$HTMLPaneEditorKit" );
//setEditorKitForContentType( "text/html", new HTMLPaneEditorKit() );
setEditorKit( new HTMLPaneEditorKit() );
setContentType ( "text/html" );
// modified
//addHyperlinkListener ( new HyperlinkHandler() );
//setEditable ( false );
// Set up to shut down pages gracefully when the parent window
// shuts down. Note that this doesn't work when somebody closes
// the outermost frame by clicking the "X" box, because the
// handler for that event (which typically calls System.exit()) is
// processed before the ancestor event is generated. This omission
// is not a big deal, since the program is shutting down anyway,
// but don't put a println in the following code and be surprised
// when it doesn't get executed.
addAncestorListener
( new AncestorAdapter()
{ public void ancestorAdded(AncestorEvent e)
{ for( Component c=HTMLPane.this; c!=null; c=c.getParent())
{ if( c instanceof Window )
{ ((Window)c).addWindowListener
( new WindowAdapter()
{ public void windowClosing(WindowEvent e)
{ handlePageShutdown();
}
}
);
break;
}
}
}
}
);
}
/**
* Since destructors aren't possible, provide a method to handle page
* shut down. Notify the JComponents associated with the custom tags that
* the page is shutting down and do some local housekeeping.
*/
protected void handlePageShutdown()
{
for( Iterator i = contributors.iterator(); i.hasNext(); )
((TagBehavior) i.next() ).destroy();
contributors.clear();
}
/************************************************************************
* If the argument is true, pre-install all custom-tag handlers
* <a href="#customTags">described earlier</a>,
* otherwise install none of the custom-tag handlers.
* The no-arg constructor doesn't install any handlers, so
* <code>new HTMLPane()</code> and <code>new HTMLPane(false)</code>
* are equivalent.
* @param installDefaultTags
*/
public HTMLPane( boolean installDefaultTags )
{ this();
if( installDefaultTags )
{ addTag( "size" , new SizeHandler() );
addTag( "inputAction", new InputActionHandler(this));
addTag( "inputNumber", new InputNumberHandler() );
addTag( "inputDate" , new InputDateHandler() );
}
}
//@constructor-end
/**
* The {@link JEditorPane} uses an editor kits to get a factory of
* {@link View} objects, each of which is responsible for rendering
* an HTML element on the screen. This kit returns a factory that
* creates custom views, and it also modifies the behavior of the
* underlying {@link Document} slightly.
*/
public class HTMLPaneEditorKit extends HTMLEditorKit
{
public ViewFactory getViewFactory()
{ return new CustomViewFactory();
}
public Document createDefaultDocument()
{ //HTMLDocument doc = (HTMLDocument)( super.createDefaultDocument() );
// <0 for synchronous load. Delays the pop up, but allows
// tags that set window size, etc. Default value is 4.
//doc.setAsynchronousLoadPriority(-1);
// The number of tokens to buffer before displaying them.
// A smaller number makes the screen to appear a bit more
// responsive because the system doesn't pause for a long
// time before displaying anything.
//
// doc.setTokenThreshold(10);
//
// This is the default value. If it's false, then
// custom tags won't be recognized.
//
// doc.setPreservesUnknownTags( true );
// the createDefaultDocument in the EkitCore
StyleSheet styles = getStyleSheet();
StyleSheet ss = new StyleSheet();
ss.addStyleSheet(styles);
ExtendedHTMLDocument doc = new ExtendedHTMLDocument(ss);
doc.setParser(getParser());
doc.setAsynchronousLoadPriority(4);
doc.setTokenThreshold(100);
return doc;
//return doc;
}
/** Return a parser that wraps the real one. This is the
* only convenient way to get a handle to the input
* stream that the parser uses: Supply an input-stream
* decorator that preprocesses the input before
* the parser reads it.
* <p>Using a preprocessor at all
* is a kludge, but creating a DOM from user
* supplied HTML and inserting it into the
* HTMLDocument is nasty, and probably slower.
* <p>
* <code>Parser</code> and <code>ParserCallback</code>
* are both inner classes of the {@link HTMLEditorKit}
* base class.
*/
protected Parser getParser()
{ final Parser p = super.getParser();
return new Parser()
{ public void parse( Reader r,
ParserCallback callBack,
boolean ignoreCharSet
) throws IOException
{ p.parse( filterProvider.inputFilter(r),
callBack, ignoreCharSet );
}
};
}
}
//@editor-kit-end
/*******************************************************************
* Create Views for the various HTML elements. This factory differs from
* the standard one in that it can create views that handle the
* modifications that I've made to EditorKit. For the most part, it
* just delegates to its base class.
*/
private final class CustomViewFactory extends HTMLEditorKit.HTMLFactory
{
// Create views for elements.
// Note that the views are not created as the elements
// are encountered; rather, they're created more or less
// at random as the elements are displayed. Don't do anything here
// that depends on the order in which elements appear in the input.
//
// Also note that undefined start-element tags are not in any way
// linked to the matching end-element tag. They two might move
// around arbitrarily.
public View create(Element element)
{ // dumpElement( element );
HTML.Tag kind = (HTML.Tag)(
element.getAttributes().getAttribute(
javax.swing.text.StyleConstants.NameAttribute) );
if( (kind==HTML.Tag.INPUT) || (kind==HTML.Tag.SELECT)
|| (kind==HTML.Tag.TEXTAREA) )
{
// Create special views that understand Forms and
// route submit operations to form observers only
// if observers are registered.
//
FormView view = (actionListeners != null)
? new LocalFormView( element )
: (FormView)( super.create(element) )
;
String type = (String)( element.getAttributes().
getAttribute(HTML.Attribute.TYPE));
return view;
}
else if( kind instanceof HTML.UnknownTag )
{ // Handling a custom element. End tags are silently ignored.
if( element.getAttributes().
getAttribute(HTML.Attribute.ENDTAG) == null)
{
final Component view = doTag( element );
if( view != null )
{ return new ComponentView(element)
{ protected Component createComponent()
{ return view;
}
};
}
// else fall through and return default (invisible) View
}
} else if(kind instanceof HTML.Tag)
{ // EkitCore handles IMG tag
HTML.Tag tagType = (HTML.Tag)kind;
if(tagType == HTML.Tag.IMG)
{
return new RelativeImageView(element);
}
}
return super.create(element);
}
}
//@view-factory-end
/*******************************************************************
* Handle a request to process a custom tag. If no handler for
* a given tag is found, the fact is logged to the com.holub.ui
* logger, and the tag is ignored.
*
* @param element The element that we're handling
*
*
* @return a JComponent to use as the view or <code>null</code>
* if there is no view.
*/
private final Component doTag( Element element )
{
if( element == null ) // it does happen!
return null;
String name = element.getName();
if( name == null )
name = "Unknown" ;
// Extract the attributes and tag name from the Element:
Properties attributes = new Properties();
AttributeSet set = element.getAttributes();
for( Enumeration i = set.getAttributeNames(); i.hasMoreElements(); )
{ Object current = i.nextElement ();
String attributeName = current.toString ();
Object attributeValue = set.getAttribute (current);
attributes.put
( (current instanceof StyleConstants)? TAG_NAME: attributeName,
attributeValue.toString()
);
}
// Now look for a handler for the tag. If there isn't one, just
// return null, which effectively causes the tag to be ignored.
TagHandler handler = (TagHandler)( tagHandlers.get(name) );
if( handler == null )
{ log.warning( "Couldn't find handler for <" +name+ ">" );
return null;
}
// There is a handler, call it to do the work. Return whatever
// component the handler returns.
JComponent component = handler.handleTag(this,attributes);
if( component instanceof TagBehavior )
contributors.add( component );
return ( component );
}
//@do-tag-end
/*******************************************************************
* Special handling for elements that can occur inside forms.
*/
public final class LocalFormView extends javax.swing.text.html.FormView
{
public LocalFormView( Element element )
{ super(element);
}
/** Chase up through the form hierarchy to find the
* <code><form></code> tag that encloses the current
* <code><input></code> tag. There's a similar
* method in the base class, but it's private so I can't use it.
*/
private Element findFormTag()
{ for(Element e=getElement(); e != null; e=e.getParentElement() )
if(e.getAttributes().getAttribute(StyleConstants.NameAttribute)
==HTML.Tag.FORM )
return e;
throw new Error("HTMLPane.LocalFormView Can't find <form>");
}
/** Override the base-class method that actually submits the form
* data to process it locally instead if the URL in the action
* field matches the "local" URL.
*/
protected void submitData(String data)
{ //System.out.println("Data [" + data + "]");
AttributeSet attributes = findFormTag().getAttributes();
String action =
(String)attributes.getAttribute(HTML.Attribute.ACTION);
String method =
(String)attributes.getAttribute(HTML.Attribute.METHOD);
String name =
(String)attributes.getAttribute(HTML.Attribute.NAME);
if( action == null ) action = "";
if( method == null ) method = "";
if (name == null) name = "";
// modified
//handleSubmit( method.toLowerCase(), action, data );
handleSubmit( method.toLowerCase(), action, name, data );
}
/** Override the base-class image-submit-button class. Given the tag:
* <PRE>
* <input type="image" src="grouchoGlasses.gif"
* name=groucho value="groucho.pressed">
* </PRE>
* The data will hold only two properties:
* <PRE>
* groucho.y=23
* groucho.x=58
* </PRE>
* Where 23 and 58 are the image-relative positions of the mouse when
* the user clicked. (Note that the value= field is ignored.)
* Image tags are useful primarily for implementing a cancel button.
* <p>
* This method does nothing but chain to the standard submit-
* processing code, which can figure out what's going on by
* looking at the attribute names.
*/
protected void imageSubmit(String data)
{ submitData(data);
}
/** Special processing for the reset button. I really want to
* override the base-class resetForm() method, but it's package-
* access restricted to javax.swing.text.html.
* I can, however, override the actionPerformed method
* that calls resetForm() (Ugh).
*/
public void actionPerformed( ActionEvent e )
{ String type = (String)( getElement().getAttributes().
getAttribute(HTML.Attribute.TYPE));
if( type.equals("reset") )
doReset( );
super.actionPerformed(e);
}
/** Make transparent any standard components used for input.
* The default components are all opaque with a gray (0xd0d0d0)
* background, which looks awful when you've set the background
* color to something else (or set it to an image) in the
* <code><body></code> tag. Setting opaque-mode
* off lets the specified background color show through the
* <code><input></code> fields.
*/
// modified
/*protected Component createComponent()
{ JComponent widget = (JComponent)( super.createComponent() );
// The widget can be null for things like type=hidden fields
if( widget != null )
{
if( !(widget instanceof JButton) )
widget.setOpaque(false);
// Adjust the alignment of everything except multiline text
// fields so that the control straddles the text baseline
// instead of sitting on it. This adjustment will make
// buttons and the text within a single-line text-input
// field align vertically with any adjacent text.
if( !(widget instanceof JScrollPane) ) // <input>
{ widget.setAlignmentY( BASELINE_ALIGNMENT );
}
else
{ // a JList is a <select>, a JTextArea is a <textarea>
Component contained =
((JScrollPane)widget).getViewport().getView();
// If it's a select, change the width from the default
// (full screen) to a bit wider than the actual contained
// text.
if( contained instanceof JList )
{ widget.setSize( contained.getPreferredSize() );
Dimension idealSize = contained.getPreferredSize();
idealSize.width += 20;
idealSize.height += 5;
widget.setMinimumSize ( idealSize );
widget.setMaximumSize ( idealSize );
widget.setPreferredSize( idealSize );
widget.setSize ( idealSize );
}
}
}
return widget;
}*/
}
//@local-form-view-end
/*******************************************************************
* Used by {@link HTMLPane} to pass form-submission information to
* any ActionListener objects.
* When a form is submitted by the user, an actionPerformed() message
* that carries a FormActionEvent is sent to all registered
* action listeners. They can use the event object to get the
* method and action attributes of the form tag as well as the
* set of data provided by the form elements.
*/
public class FormActionEvent extends ActionEvent
{ private final String method;
private final String action;
private final String name;
private final Properties data = new Properties();
// modified
private FormActionEvent( String method, String action, String name, String data){
super( HTMLPane.this, 0, "submit" );
this.method = method;
this.action = action;
this.name = name;
try
{
data = UrlUtil.decodeUrlEncoding(data) + "\n" + dataFromContributors();
this.data.load( new ByteArrayInputStream( data.getBytes()) );
}
catch( IOException e )
{ //assert false : "\"Impossible\" IOException";
}
}
/**
* @param method method= attribute to Form tag.
* @param action action= attribute to Form tag.
* @param data Data provided by standard HTML element. Data
* provided by custom tags is appended to this set.
*/
private FormActionEvent( String method, String action, String data )
{ super( HTMLPane.this, 0, "submit" );
this.method = method;
this.action = action;
this.name = null;
try
{
data = UrlUtil.decodeUrlEncoding(data) + "\n" + dataFromContributors();
this.data.load( new ByteArrayInputStream( data.getBytes()) );
}
catch( IOException e )
{ //assert false : "\"Impossible\" IOException";
}
}
/** Return the method= attribute of the <form> tag */
public String method() { return method; }
/** Return the action= attribute of the <form> tag */
public String action() { return action; }
public String name() { return name; }
/** Return the a set of properties representing the name=value
* pairs that would be sent to the server on form submission.
*/
public Properties data() { return data; }
/** Convenience method, works the same as
* <code>(HTMLPane)( event.getSource() )</code>
*/
public HTMLPane source(){ return (HTMLPane)getSource(); }
}
//@form-action-event-end
//-----------------------------------------------------------------
public final void addActionListener( ActionListener listener )
{ actionListeners = AWTEventMulticaster.add(actionListeners, listener);
}
public final void removeActionListener( ActionListener listener )
{ actionListeners = AWTEventMulticaster.remove(actionListeners, listener);
}
private final void handleSubmit(final String method, final String action,
final String data )
{ actionListeners.actionPerformed(
new FormActionEvent
( method,
action,
UrlUtil.decodeUrlEncoding(data)
+ "\n"
+ dataFromContributors()
)
);
}
// modified
private final void handleSubmit(final String method, final String action,
final String name, final String data )
{ actionListeners.actionPerformed(
new FormActionEvent(method, action, name,
UrlUtil.decodeUrlEncoding(data)
+ "\n"
+ dataFromContributors())
);
}
/*package*/ final void handleInputActionTag( final String name )
{ actionListeners.actionPerformed(new FormActionEvent( "", "", "" ));
}
/*** ****************************************************************
* Add support for a custom tag.
* Custom tags may contain arbitrary attributes (which are passed to your
* handler in a {@link Properties} object). Elements specified
* by a custom tag may contain plain-text contents;
* any nested elements are discarded. The end-tag element is
* <u>required</u> in the input, since your handler isn't called
* until the end-tag element is found.
* <p>
* Only one handler can be registered for a given tag.
* If you register more than one, then the most recent registration wins.
* All tag handlers must be installed before the page is loaded
* by {@link #setText}, {@link #setPage}, or equivalent.
* <p>
* The handler can choose to return a JComponent, which will appear on
* the rendered page in place of the tag. By default, the component
* sits on the text baseline. Use {@link JComponent#setAlignmentY}
* to control the placement relative to the text baseline. A value of
* 0.75 will place 75% of the Component above the baseline, for example.
* (a value of .85 is about right for a JLabel in the default font.)
* Here's a custom tag
* <code><holub:label text="<i>TEXT</i>"></code>
* that is replaced by a JLabel that holds the <i>TEXT</i> string:
* <PRE>
* pane.addTag
* ( "myTag",
* new TagHandler()
* { public JComponent
* handleTag(HTMLPane source, Properties attributes)
* { JComponent view = new JLabel(attributes.getProperty("text"));
* view.setAlignmentY(0.85F);
* return view;
* }
* }
* );
* </PRE>
* Tag handlers are shared by <u>all</u> instances of HtmlFrame.
* <p>
* Note that <b>tags are not case sensitive</b>, so regardless
* of how the tag appears in the input (or in the tag argument),
* it's treated as if it's all lower-case characters.
*
* @param tag the tag name (without any "angle" brackets).
* Must be all-upper-case or all-lower-case
* letters or underscores (no colons, no mixed case).
* Normal HTML tags cannot be redefined.
* Because of the way that the {@link JEditorPane} base
* class works,
* you can't use use "p-implied" or "content" as a custom-tag
* name and you can't use "endtag=" as an attribute.
* If assertions are enabled, attempts to register a
* standard tag generate <code>AssertionError</code>s, but if
* assertions are not enabled, the request is logged,
* but is otherwise ignored (and will form a minor memory leak.)
*
* @param handler The handler to call when the tag is encountered.
* @see TagHandler
*/
public final void addTag(String tag, TagHandler handler)
{ tag = tag.toLowerCase();
//assert !isStandardHTMLtag(tag) :
// "Illegal attempt to redefine standard HTML tag <"+tag+">";
//log.info( "Adding custom tag <" + tag + ">" );
tagHandlers.put( tag, handler );
}
/** Remove the handler for a given tag. Once this call is made,
* the tag will be ignored if it's encountered in a newly-loaded
* page. (A warning is logged if an unexpected tag is
* encountered, but this situation is not considered to be a
* run-time error.)
*/
public final void removeTag( String tag )
{ tagHandlers.remove( tag );
}
/** Notify the JComponents associated with the custom tags that
* the user has hit the reset button.
*/
protected void doReset()
{ for( Iterator i = contributors.iterator(); i.hasNext(); )
((TagBehavior) i.next() ).reset();
}
/** Collect form data from the JComponents associated with the
* custom tags and append it to the data set passed to the
* form handlers. This method has been made protected so that
* overrides can do some sort of processing or filtering
* on the data if they need to.
* @return a set of newline-delimited key=value pairs contained
* in a single String.
*/
protected String dataFromContributors()
{
StringBuffer formData = new StringBuffer();
for(Iterator i = contributors.iterator(); i.hasNext(); )
{ formData.append( ((TagBehavior) i.next() ).getFormData() );
formData.append("\n");
}
return formData.toString();
}
/** Workhorse function used by the assertion at the top of
* {@link #addTag}. You can use this method to see
* if a tag is available before attempting to register it.
* Note that the tag must be specified using all-lower-case
* characters.
*
* @return <code>true</code> if the tag argument specifies a standard
* HTML tag or one of the internal tags "p-implied" or "content."
* Otherwise, return <code>false</code>.
*/
public static final boolean isStandardHTMLtag(String tag)
{ HTML.Tag[] allTags = HTML.getAllTags();
if( tag.equals("p-implied") || tag.equals("content") )
return true;
for( int i = 0; i < allTags.length; ++i )
if( allTags[i].toString().toLowerCase().equals(tag) )
return true;
return false;
}
/*******************************************************************
* Provide input preprocessing.
* Use this method to replace the default input filter
* {@link FilterFactory#NULL_FACTORY}.
*
* @see FilterFactory
* @see NamespaceFilterFactory
*/
public void filterInput( FilterFactory provider )
{ filterProvider = provider;
}
/**
* A convenience method for local testing of a UI that will eventually
* be web based. If you map your home URL to a local directory,
* all links to the home URL are automatically replaced by
* links to the directory. For example, given
* <PRE>
* myPane.addHostMapping( "www.holub.com", "file://c:/src/test" );
* </PRE>
* an HTML "anchor" that looks like this:
* <PRE>
* <a href="http://www.holub.com/dir/foo.html">
* </PRE>
* is treated as if you had specified:
* <PRE>
* <a href="file://c:/src/test/dir/foo.html">
* </PRE>
* Multiple mappings are supported. That is, if you call this method
* more than once, then all the mappings you specify apply.
* <p>
* The host mappings are shared by <u>all</u> instances of
* HtmlFrame, including popups created by target=_blank in a
* hyperlink.
*
* @see #removeHostMapping
*/
public static final void addHostMapping( String thisHost,
String mapsToThisLocation )
{ if( hostMap == null )
hostMap = new HashMap();
hostMap.put( thisHost,
new HostMapping(thisHost, mapsToThisLocation));
}
/** Remove a host mapping added by a previous call to
* {@link #addHostMapping}.
* @see #addHostMapping
*/
public static final void removeHostMapping( String thisHost )
{ hostMap.remove(thisHost);
}
/** Map a url if there's an entry for the host portion in the host map.
* @param url a URL specified in terms of the "host" location.
* @return a URL for the mapped location.
* @see #addHostMapping
* @see #removeHostMapping
*/
public URL map(URL url)
{ if( hostMap != null )
{ HostMapping mapping = (HostMapping) hostMap.get(url.getHost());
if( mapping != null )
url = mapping.map( url );
}
return url;
}
/*******************************************************************
* This class handles the host-to-local-file mapping mechanics
* for {@link #addHostMapping}. The {@link #hostMap}
* table is a java.util.Map of objects of this class.
*/
private static final class HostMapping
{ private final String remote;
private final String local;
public HostMapping( String remote, String local )
{ this.remote = ".*://" + remote.replaceAll("\\.", "\\.");
this.local = local ;
}
public URL map( URL page )
{ try
{ String s = page.toExternalForm();
return new URL( s.replaceFirst(remote, local) );
}
catch( MalformedURLException e )
{ log.warning( "Couldn't map " + remote + " to "
+ local + ": " + e.getMessage() );
}
return page;
}
}
//@hyperlink-handler-start
/************************************************************************
* This Hyperlink handler replaces the contents of the current pane
* with whatever's at the indicated link. This code is copied more or
* less verbatim from the Sun documentation. I've also added simple
* support for the mailto: protocol.
*/
private final class HyperlinkHandler implements HyperlinkListener
{ public void hyperlinkUpdate(HyperlinkEvent event)
{ try
{ if( event.getEventType() == HyperlinkEvent.EventType.ACTIVATED )
{
HTMLPane source = (HTMLPane)event.getSource();
String description = event.getDescription();
Element e = event.getSourceElement();
// Get the attributes of the <a ...> tag that got
// us here, then extract the target= attribute. If
// we find target=_blank, then display the page
// in a popup window. I'm assuming that the
// href references an html file, because it wouldn't
// make much sense to use target=_blank if it didn't.
AttributeSet tagAttributes = (AttributeSet)
(e.getAttributes().getAttribute(HTML.Tag.A));
String target = null;
if( tagAttributes != null )
target = (String) tagAttributes.getAttribute(
HTML.Attribute.TARGET);
if( target != null && target.equals("_blank") )
{ popupBrowser( event.getURL() );
return;
}
// Handle http: and file: links. If the description
// doesn't contain a protocol (there's no ':'),
// then assume a "relative" link:
if( description.startsWith("http:")
|| description.startsWith("file:")
|| description.indexOf(":") == -1
)
{
JEditorPane pane = (JEditorPane) event.getSource();
if(event instanceof HTMLFrameHyperlinkEvent)
{ ((HTMLDocument)(source.getDocument())).
processHTMLFrameHyperlinkEvent(
(HTMLFrameHyperlinkEvent)event);
}
else if( !redirect(event.getURL()) )
{ unknownRedirect(event.getDescription());
}
}
else if( description.startsWith("mailto") )
{ if( isWindows() )
Runtime.getRuntime().exec(
"cmd.exe /c start " + description);
else
unknownRedirect(event.getDescription());
}
else
{ unknownRedirect( event.getDescription() );
}
}
}
catch( Exception e )
{ log.warning
( "Unexpected exception caught while processing hyperlink: "
+ e.toString() + "\n"
+ Log.stackTraceAsString( e )
);
}
}
/** Used for anchor tags that include target=_blank attributes. Pop up
* a new instance of an HTMLPanel in a subwindow, displaying the page
* whose URL is specified in the "description" argument.
*/
private final void popupBrowser( URL target )
throws MalformedURLException
{ JFrame popupFrame = new JFrame();
HTMLPane popup = new HTMLPane();
popupFrame.getContentPane(). add( new JScrollPane(popup) );
popup.redirect(target);
Dimension size = popup.getPreferredSize();
Point location = getLocationOnScreen();
popupFrame.setBounds( location.x + 10,
location.y + 10,
size.width,
size.height );
popupFrame.show();
}
/** Mailto support is OS specific, so we need to know the OS.
*/
private final boolean isWindows()
{ return( System.getProperty("os.name").toLowerCase().
indexOf("windows") != -1);
}
}
/*******************************************************************
* Handles hyperlinks for the http:// or file:// protocols.
* Not all files are loaded, and a name-based algorithm
* is used to determine what gets loaded and what doesn't
* get loaded.
* All requests that are not handled here (i.e. for a protocol
* other than http: or file:, or for a file with an extension
* not on the list, below) are routed to
* {@link #unknownRedirect(String)}
* for processing.
* Overload the current method to change the way that the
* http: and file: protocols are processed.
* Overload {@link #unknownRedirect(String)} to add support
* for other protocols.
* <p>
* Any host mappings specified to
* {@link #addHostMapping} are processed first.
* Then, those URLs that end in
* one of the following extensions are handled.
* <table border=0 cellspacing=0 cellpadding=0>
* <tr><td><code>.shtml</code></td><td> HTML file </td></tr>
* <tr><td><code>.html </code></td><td> HTML file </td></tr>
* <tr><td><code>.htm </code></td><td> HTML file </td></tr>
* <tr><td><code>.pl </code></td><td> Perl script </td></tr>
* <tr><td><code>.jsp </code></td><td> Java Server Page </td></tr>
* <tr><td><code>.asp </code></td><td> Active Server Page </td></tr>
* <tr><td><code>.php </code></td><td> PHP script </td></tr>
* <tr><td><code>.py </code></td><td> Python Script </td></tr>
* </table>
* <p>
* All URL's whose paths do not have a '.' in the string that follows the
* rightmost slash (including directory specifications, which should be
* terminated by a slash) are assumed to reference an implicit index.html
* file. Similarly, a URL that specifies a host or directory, but no file
* (e.g. "http://www.holub.com" or "http://www.holub.com/directory/),
* is loaded.
* <p>
* N.B.: A relative file that doesn't have an extension (such as
* <code><a href=filename></code> is not recognized as
* an HTML file, so is not processed.
* <p>
* You can override this method if you need to change this default
* behavior, This method is called from the Swing Event Thread, so
* it's safe for your override to use
* {@link JEditorPane#setPage(URL)}
*
* @param page the target page. Must be a file: or http: URL.
* @return true if the page was handled. If you return false,
* {@link #unknownRedirect(String)} is given a chance
* to process it.
*/
public boolean redirect( URL page )
{ //assert( page.getProtocol().equals("http")
// || page.getProtocol().equals("file") );
try
{ String file = page.getFile();
if( !(file.length()==0 || htmlExtensions.matcher(file).matches()) )
return false;
setPage( page ); // local version of setPage handles host mapping
}
catch (Throwable t)
{ String message = "HTMLPane couldn't open hyperlink: "
+ t.getMessage();
log.warning( message );
JOptionPane.showMessageDialog(
HTMLPane.this,
message,
"401 Error",
JOptionPane.WARNING_MESSAGE );
}
return true;
}
/** A regular expression that identifies all file extensions
* recognized by {@link #redirect(URL)}
* as an HTML file. The same expression also recognizes
* directories (that end in a slash) and all file names
* that don't have an extension.
*/
private static final Pattern htmlExtensions =
Pattern.compile( "(.*\\.(html|htm|pl|jsp|asp|php|py|shtml)([?#].*)?$)"
+ "|(.*/[^\\./]+([?#].*)?$)"
+ "|(.*/([?#].*)?$)",
Pattern.CASE_INSENSITIVE );
/** Handles all hyperlink protocols that aren't recognized by
* {@link #redirect redirect(...)}.
* Is also called if a mailto: protocol is specified and
* we're not running under Windows. This implementation just logs
* a warning to "com.holub.ui" and pops up a warning-style dialog box
* indicating that the protocol isn't supported. You can override
* this method to support protocols other than file:// and http://
*/
public void unknownRedirect( String request )
{ log.warning("HTMLPane: Protocol or file type not supported ("
+ request + ")" );
JOptionPane.showMessageDialog
( HTMLPane.this,
"Protocol or file type not supported (" + request + ")",
"Link Error",
JOptionPane.WARNING_MESSAGE
);
}
//@hyperlink-handler-end
/*******************************************************************
* Overrides {@link JEditorPane#setText(String)}
* to do general housekeeping.
* @see #setPage(URL)
* @see #setPage(String)
*/
public final void setText(String text)
{ handlePageShutdown();
super.setText(text);
}
/** Overrides {@link JEditorPane#setPage(URL)} to
* map hosts, and do general housekeeping.
* @see #setText(String)
* @see #setPage(String)
*/
public final void setPage(URL url) throws IOException
{ handlePageShutdown();
super.setPage( map(url) );
}
/** This version of setPage is disabled in an HTMLPane. You can
* use setPage(new URL(...)); if all you have is a string.
* @see #setText(String)
* @see #setPage(URL)
* @throws UnsupportedOperationException always
*/
public final void setPage(String location)
{ throw new UnsupportedOperationException(
"setPage(String) not supported by HTMLPane" );
}
/** Read is disabled in an HTMLPane.
* @throws UnsupportedOperationException always
*/
public void read(InputStream in, Object desc) throws IOException
{ throw new UnsupportedOperationException(
"read() not supported by HTMLPane" );
}
/** A version of {@link JEditorPane#setPage(URL)}
* that can be called safely from somewhere other than a Swing
* event handler. Note that your form handlers <i>are</i>
* being called from an event handler.
*/
public void setPageAsynchronously(final URL page) throws IOException
{ SwingUtilities.invokeLater
( new Runnable()
{ public void run()
{ try
{ setPage(page);
}
catch(IOException e)
{ log.warning("HTMLPane: setPage() failed on Event Thread");
}
}
}
);
}
//@end
/*******************************************************************
* A test class. Creates pages using test/main.html and
* test/submit.html. This test is, unfortunately, highly interactive.
*/
private static class Test
{
static
{
try
{ UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName() );
}
catch( Exception e )
{ e.printStackTrace();
System.exit(-1);
}
}
public static void main( String[] args )
{
try
{ // Use the Windows look and feel.
URL mainPage;
mainPage =new URL( args.length >= 1
? args[0]
: "file:/c:/src/com/holub/ui/HTML/test/main.html" );
Log.toScreen("com.holub.ui");
final HTMLPane pane = new HTMLPane();
pane.addActionListener
( new ActionListener()
{ public void actionPerformed( ActionEvent event )
{
FormActionEvent act = (FormActionEvent)event;
act.data().list(System.out);
String value = act.data().getProperty( "value" );
if( value != null && value.equals("Cancel") )
{ System.out.println("CANCEL");
}
else
{ System.out.println("\n"+ act.getActionCommand() );
System.out.println("\t"+"method=" + act.method() );
System.out.println("\t"+"action=" + act.action() );
act.data().list( System.out );
System.out.println("");
try
{ act.source().setPage
( new URL(
"file:/c:/src/com/holub/ui/HTML/test/submit.html")
);
}
catch( Exception e )
{ e.printStackTrace();
}
System.out.println("");
}
}
}
);
pane.addHostMapping( "www.test.com",
"file:/c:/src/com/holub/ui/test" );
// should fail with an assertion because of null handler
try { pane.addTag( "B", null ); } catch(AssertionError e){}
pane.addTag
( "size",
new TagHandler()
{ public JComponent
handleTag(HTMLPane source, Properties attributes)
{ source.setPreferredSize
( new Dimension
( Integer.parseInt(attributes.getProperty("width")),
Integer.parseInt(attributes.getProperty("height"))
)
);
return null;
}
}
);
// Converts tags of the form <abcd:efg> into <abcd_efg>
// The algorithm looks for a <, followed by non-white
// characters other than >, followed by a :, and replaces
// the : with an _.
pane.filterInput( new NamespaceFilterFactory() );
pane.addTag // handle <holub:JLabel> tags
( "holub_JLabel",
new TagHandler()
{ public JComponent
handleTag(HTMLPane source,Properties a)
{ JComponent view = new JLabel(a.getProperty("text"));
view.setAlignmentY(0.85F);
return view;
}
}
);
class ContributingText extends JTextField implements TagBehavior
{ private final String value;
private final String name;
public ContributingText(String name, String value)
{ super(value);
this.name =name;
this.value =value;
}
public void reset()
{
setText( value );
invalidate();
}
public void destroy()
{ System.out.println("Destroying <textInput> control");
}
public String getFormData ()
{ return name + "=" + getText();
};
public Dimension getPreferredSize()
{ return new Dimension(150,20);
}
public Dimension getMinimumSize()
{ return getPreferredSize();
}
public Dimension getMaximumSize()
{ return getPreferredSize();
}
}
pane.addTag
( "textInput",
new TagHandler()
{ public JComponent
handleTag(HTMLPane source,Properties attributes)
{ return new ContributingText
( attributes.getProperty("name"),
attributes.getProperty("value") );
}
}
);
pane.addTag
( "test",
new TagHandler()
{ public JComponent
handleTag(HTMLPane source,Properties attr)
{ System.out.println
( "\n<"
+ attr.getProperty(HTMLPane.TAG_NAME)
+">"
);
attr.list( System.out );
return null;
}
}
);
JFrame frame = new JFrame();
frame.addWindowListener
( new WindowAdapter()
{ public void windowClosing( WindowEvent e )
{ System.exit(255);
}
}
);
frame.getContentPane().add( new JScrollPane(pane) );
try
{ pane.setPage( mainPage );
}
catch(Exception e)
{ System.err.println( "Can't open " + mainPage + "\n" );
e.printStackTrace();
}
frame.setSize( pane.getPreferredSize() );
frame.pack();
frame.show();
/*
createSmallFrame
( new Runnable()
{ public void run()
{ createSmallFrame(null);
}
}
);
*/
}
catch( Throwable e )
{ e.printStackTrace();
}
}
}
// Create a small frame. When it shuts down (is submitted)
// it executes r.run() [before disposing the current window]
//
private static void createSmallFrame( final Runnable r )
{
try
{
final JFrame frame = new JFrame();
final HTMLPane pane = new HTMLPane();
pane.addActionListener
( new ActionListener()
{ public void actionPerformed(ActionEvent e)
{
if( r != null )
r.run();
frame.setVisible(false);
frame.dispose();
}
}
);
pane.setPage(
new URL("file:/c:/src/com/holub/ui/HTML/test/second.html"));
frame.getContentPane().add( pane );
frame.pack();
frame.show();
}
catch(Exception e)
{ e.printStackTrace();
}
}
}
//@end