1 /*
   2  * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text.html;
  26 
  27 import sun.awt.AppContext;
  28 
  29 import java.awt.*;
  30 import java.awt.event.*;
  31 import java.io.*;
  32 import java.net.MalformedURLException;
  33 import java.net.URL;
  34 import javax.swing.text.*;
  35 import javax.swing.*;
  36 import javax.swing.event.*;
  37 import javax.swing.plaf.TextUI;
  38 import java.util.*;
  39 import javax.accessibility.*;
  40 import java.lang.ref.*;
  41 import java.security.AccessController;
  42 import java.security.PrivilegedAction;
  43 
  44 /**
  45  * The Swing JEditorPane text component supports different kinds
  46  * of content via a plug-in mechanism called an EditorKit.  Because
  47  * HTML is a very popular format of content, some support is provided
  48  * by default.  The default support is provided by this class, which
  49  * supports HTML version 3.2 (with some extensions), and is migrating
  50  * toward version 4.0.
  51  * The <applet> tag is not supported, but some support is provided
  52  * for the <object> tag.
  53  * <p>
  54  * There are several goals of the HTML EditorKit provided, that have
  55  * an effect upon the way that HTML is modeled.  These
  56  * have influenced its design in a substantial way.
  57  * <dl>
  58  * <dt>
  59  * Support editing
  60  * <dd>
  61  * It might seem fairly obvious that a plug-in for JEditorPane
  62  * should provide editing support, but that fact has several
  63  * design considerations.  There are a substantial number of HTML
  64  * documents that don't properly conform to an HTML specification.
  65  * These must be normalized somewhat into a correct form if one
  66  * is to edit them.  Additionally, users don't like to be presented
  67  * with an excessive amount of structure editing, so using traditional
  68  * text editing gestures is preferred over using the HTML structure
  69  * exactly as defined in the HTML document.
  70  * <p>
  71  * The modeling of HTML is provided by the class <code>HTMLDocument</code>.
  72  * Its documentation describes the details of how the HTML is modeled.
  73  * The editing support leverages heavily off of the text package.
  74  *
  75  * <dt>
  76  * Extendable/Scalable
  77  * <dd>
  78  * To maximize the usefulness of this kit, a great deal of effort
  79  * has gone into making it extendable.  These are some of the
  80  * features.
  81  * <ol>
  82  *   <li>
  83  *   The parser is replaceable.  The default parser is the Hot Java
  84  *   parser which is DTD based.  A different DTD can be used, or an
  85  *   entirely different parser can be used.  To change the parser,
  86  *   reimplement the getParser method.  The default parser is
  87  *   dynamically loaded when first asked for, so the class files
  88  *   will never be loaded if an alternative parser is used.  The
  89  *   default parser is in a separate package called parser below
  90  *   this package.
  91  *   <li>
  92  *   The parser drives the ParserCallback, which is provided by
  93  *   HTMLDocument.  To change the callback, subclass HTMLDocument
  94  *   and reimplement the createDefaultDocument method to return
  95  *   document that produces a different reader.  The reader controls
  96  *   how the document is structured.  Although the Document provides
  97  *   HTML support by default, there is nothing preventing support of
  98  *   non-HTML tags that result in alternative element structures.
  99  *   <li>
 100  *   The default view of the models are provided as a hierarchy of
 101  *   View implementations, so one can easily customize how a particular
 102  *   element is displayed or add capabilities for new kinds of elements
 103  *   by providing new View implementations.  The default set of views
 104  *   are provided by the <code>HTMLFactory</code> class.  This can
 105  *   be easily changed by subclassing or replacing the HTMLFactory
 106  *   and reimplementing the getViewFactory method to return the alternative
 107  *   factory.
 108  *   <li>
 109  *   The View implementations work primarily off of CSS attributes,
 110  *   which are kept in the views.  This makes it possible to have
 111  *   multiple views mapped over the same model that appear substantially
 112  *   different.  This can be especially useful for printing.  For
 113  *   most HTML attributes, the HTML attributes are converted to CSS
 114  *   attributes for display.  This helps make the View implementations
 115  *   more general purpose
 116  * </ol>
 117  *
 118  * <dt>
 119  * Asynchronous Loading
 120  * <dd>
 121  * Larger documents involve a lot of parsing and take some time
 122  * to load.  By default, this kit produces documents that will be
 123  * loaded asynchronously if loaded using <code>JEditorPane.setPage</code>.
 124  * This is controlled by a property on the document.  The method
 125  * {@link #createDefaultDocument createDefaultDocument} can
 126  * be overriden to change this.  The batching of work is done
 127  * by the <code>HTMLDocument.HTMLReader</code> class.  The actual
 128  * work is done by the <code>DefaultStyledDocument</code> and
 129  * <code>AbstractDocument</code> classes in the text package.
 130  *
 131  * <dt>
 132  * Customization from current LAF
 133  * <dd>
 134  * HTML provides a well known set of features without exactly
 135  * specifying the display characteristics.  Swing has a theme
 136  * mechanism for its look-and-feel implementations.  It is desirable
 137  * for the look-and-feel to feed display characteristics into the
 138  * HTML views.  An user with poor vision for example would want
 139  * high contrast and larger than typical fonts.
 140  * <p>
 141  * The support for this is provided by the <code>StyleSheet</code>
 142  * class.  The presentation of the HTML can be heavily influenced
 143  * by the setting of the StyleSheet property on the EditorKit.
 144  *
 145  * <dt>
 146  * Not lossy
 147  * <dd>
 148  * An EditorKit has the ability to be read and save documents.
 149  * It is generally the most pleasing to users if there is no loss
 150  * of data between the two operation.  The policy of the HTMLEditorKit
 151  * will be to store things not recognized or not necessarily visible
 152  * so they can be subsequently written out.  The model of the HTML document
 153  * should therefore contain all information discovered while reading the
 154  * document.  This is constrained in some ways by the need to support
 155  * editing (i.e. incorrect documents sometimes must be normalized).
 156  * The guiding principle is that information shouldn't be lost, but
 157  * some might be synthesized to produce a more correct model or it might
 158  * be rearranged.
 159  * </dl>
 160  *
 161  * @author  Timothy Prinzing
 162  */
 163 @SuppressWarnings("serial") // Same-version serialization only
 164 public class HTMLEditorKit extends StyledEditorKit implements Accessible {
 165 
 166     private JEditorPane theEditor;
 167 
 168     /**
 169      * Constructs an HTMLEditorKit, creates a StyleContext,
 170      * and loads the style sheet.
 171      */
 172     public HTMLEditorKit() {
 173 
 174     }
 175 
 176     /**
 177      * Get the MIME type of the data that this
 178      * kit represents support for.  This kit supports
 179      * the type <code>text/html</code>.
 180      *
 181      * @return the type
 182      */
 183     public String getContentType() {
 184         return "text/html";
 185     }
 186 
 187     /**
 188      * Fetch a factory that is suitable for producing
 189      * views of any models that are produced by this
 190      * kit.
 191      *
 192      * @return the factory
 193      */
 194     public ViewFactory getViewFactory() {
 195         return defaultFactory;
 196     }
 197 
 198     /**
 199      * Create an uninitialized text storage model
 200      * that is appropriate for this type of editor.
 201      *
 202      * @return the model
 203      */
 204     public Document createDefaultDocument() {
 205         StyleSheet styles = getStyleSheet();
 206         StyleSheet ss = new StyleSheet();
 207 
 208         ss.addStyleSheet(styles);
 209 
 210         HTMLDocument doc = new HTMLDocument(ss);
 211         doc.setParser(getParser());
 212         doc.setAsynchronousLoadPriority(4);
 213         doc.setTokenThreshold(100);
 214         return doc;
 215     }
 216 
 217     /**
 218      * Try to get an HTML parser from the document.  If no parser is set for
 219      * the document, return the editor kit's default parser.  It is an error
 220      * if no parser could be obtained from the editor kit.
 221      */
 222     private Parser ensureParser(HTMLDocument doc) throws IOException {
 223         Parser p = doc.getParser();
 224         if (p == null) {
 225             p = getParser();
 226         }
 227         if (p == null) {
 228             throw new IOException("Can't load parser");
 229         }
 230         return p;
 231     }
 232 
 233     /**
 234      * Inserts content from the given stream. If <code>doc</code> is
 235      * an instance of HTMLDocument, this will read
 236      * HTML 3.2 text. Inserting HTML into a non-empty document must be inside
 237      * the body Element, if you do not insert into the body an exception will
 238      * be thrown. When inserting into a non-empty document all tags outside
 239      * of the body (head, title) will be dropped.
 240      *
 241      * @param in the stream to read from
 242      * @param doc the destination for the insertion
 243      * @param pos the location in the document to place the
 244      *   content
 245      * @exception IOException on any I/O error
 246      * @exception BadLocationException if pos represents an invalid
 247      *   location within the document
 248      * @exception RuntimeException (will eventually be a BadLocationException)
 249      *            if pos is invalid
 250      */
 251     public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
 252 
 253         if (doc instanceof HTMLDocument) {
 254             HTMLDocument hdoc = (HTMLDocument) doc;
 255             if (pos > doc.getLength()) {
 256                 throw new BadLocationException("Invalid location", pos);
 257             }
 258 
 259             Parser p = ensureParser(hdoc);
 260             ParserCallback receiver = hdoc.getReader(pos);
 261             Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective");
 262             p.parse(in, receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue());
 263             receiver.flush();
 264         } else {
 265             super.read(in, doc, pos);
 266         }
 267     }
 268 
 269     /**
 270      * Inserts HTML into an existing document.
 271      *
 272      * @param doc the document to insert into
 273      * @param offset the offset to insert HTML at
 274      * @param popDepth the number of ElementSpec.EndTagTypes to generate
 275      *                  before inserting
 276      * @param html the HTML string
 277      * @param pushDepth the number of ElementSpec.StartTagTypes with a direction
 278      *                  of ElementSpec.JoinNextDirection that should be generated
 279      *                  before inserting, but after the end tags have been generated
 280      * @param insertTag the first tag to start inserting into document
 281      *
 282      * @throws BadLocationException if {@code offset} is invalid
 283      * @throws IOException on I/O error
 284      * @exception RuntimeException (will eventually be a BadLocationException)
 285      *            if pos is invalid
 286      */
 287     public void insertHTML(HTMLDocument doc, int offset, String html,
 288                            int popDepth, int pushDepth,
 289                            HTML.Tag insertTag) throws
 290                        BadLocationException, IOException {
 291         if (offset > doc.getLength()) {
 292             throw new BadLocationException("Invalid location", offset);
 293         }
 294 
 295         Parser p = ensureParser(doc);
 296         ParserCallback receiver = doc.getReader(offset, popDepth, pushDepth,
 297                                                 insertTag);
 298         Boolean ignoreCharset = (Boolean)doc.getProperty
 299                                 ("IgnoreCharsetDirective");
 300         p.parse(new StringReader(html), receiver, (ignoreCharset == null) ?
 301                 false : ignoreCharset.booleanValue());
 302         receiver.flush();
 303     }
 304 
 305     /**
 306      * Write content from a document to the given stream
 307      * in a format appropriate for this kind of content handler.
 308      *
 309      * @param out the stream to write to
 310      * @param doc the source for the write
 311      * @param pos the location in the document to fetch the
 312      *   content
 313      * @param len the amount to write out
 314      * @exception IOException on any I/O error
 315      * @exception BadLocationException if {@code pos} represents an invalid
 316      *   location within the document
 317      */
 318     public void write(Writer out, Document doc, int pos, int len)
 319         throws IOException, BadLocationException {
 320 
 321         if (doc instanceof HTMLDocument) {
 322             HTMLWriter w = new HTMLWriter(out, (HTMLDocument)doc, pos, len);
 323             w.write();
 324         } else if (doc instanceof StyledDocument) {
 325             MinimalHTMLWriter w = new MinimalHTMLWriter(out, (StyledDocument)doc, pos, len);
 326             w.write();
 327         } else {
 328             super.write(out, doc, pos, len);
 329         }
 330     }
 331 
 332     /**
 333      * Called when the kit is being installed into the
 334      * a JEditorPane.
 335      *
 336      * @param c the JEditorPane
 337      */
 338     public void install(JEditorPane c) {
 339         c.addMouseListener(linkHandler);
 340         c.addMouseMotionListener(linkHandler);
 341         c.addCaretListener(nextLinkAction);
 342         super.install(c);
 343         theEditor = c;
 344     }
 345 
 346     /**
 347      * Called when the kit is being removed from the
 348      * JEditorPane.  This is used to unregister any
 349      * listeners that were attached.
 350      *
 351      * @param c the JEditorPane
 352      */
 353     public void deinstall(JEditorPane c) {
 354         c.removeMouseListener(linkHandler);
 355         c.removeMouseMotionListener(linkHandler);
 356         c.removeCaretListener(nextLinkAction);
 357         super.deinstall(c);
 358         theEditor = null;
 359     }
 360 
 361     /**
 362      * Default Cascading Style Sheet file that sets
 363      * up the tag views.
 364      */
 365     public static final String DEFAULT_CSS = "default.css";
 366 
 367     /**
 368      * Set the set of styles to be used to render the various
 369      * HTML elements.  These styles are specified in terms of
 370      * CSS specifications.  Each document produced by the kit
 371      * will have a copy of the sheet which it can add the
 372      * document specific styles to.  By default, the StyleSheet
 373      * specified is shared by all HTMLEditorKit instances.
 374      * This should be reimplemented to provide a finer granularity
 375      * if desired.
 376      *
 377      * @param s a StyleSheet
 378      */
 379     public void setStyleSheet(StyleSheet s) {
 380         if (s == null) {
 381             AppContext.getAppContext().remove(DEFAULT_STYLES_KEY);
 382         } else {
 383             AppContext.getAppContext().put(DEFAULT_STYLES_KEY, s);
 384         }
 385     }
 386 
 387     /**
 388      * Get the set of styles currently being used to render the
 389      * HTML elements.  By default the resource specified by
 390      * DEFAULT_CSS gets loaded, and is shared by all HTMLEditorKit
 391      * instances.
 392      *
 393      * @return the StyleSheet
 394      */
 395     public StyleSheet getStyleSheet() {
 396         AppContext appContext = AppContext.getAppContext();
 397         StyleSheet defaultStyles = (StyleSheet) appContext.get(DEFAULT_STYLES_KEY);
 398 
 399         if (defaultStyles == null) {
 400             defaultStyles = new StyleSheet();
 401             appContext.put(DEFAULT_STYLES_KEY, defaultStyles);
 402             try {
 403                 InputStream is = HTMLEditorKit.getResourceAsStream(DEFAULT_CSS);
 404                 Reader r = new BufferedReader(
 405                         new InputStreamReader(is, "ISO-8859-1"));
 406                 defaultStyles.loadRules(r, null);
 407                 r.close();
 408             } catch (Throwable e) {
 409                 // on error we simply have no styles... the html
 410                 // will look mighty wrong but still function.
 411             }
 412         }
 413         return defaultStyles;
 414     }
 415 
 416     /**
 417      * Fetch a resource relative to the HTMLEditorKit classfile.
 418      * If this is called on 1.2 the loading will occur under the
 419      * protection of a doPrivileged call to allow the HTMLEditorKit
 420      * to function when used in an applet.
 421      *
 422      * @param name the name of the resource, relative to the
 423      *             HTMLEditorKit class
 424      * @return a stream representing the resource
 425      */
 426     static InputStream getResourceAsStream(final String name) {
 427         return AccessController.doPrivileged(
 428                 new PrivilegedAction<InputStream>() {
 429                     public InputStream run() {
 430                         return HTMLEditorKit.class.getResourceAsStream(name);
 431                     }
 432                 });
 433     }
 434 
 435     /**
 436      * Fetches the command list for the editor.  This is
 437      * the list of commands supported by the superclass
 438      * augmented by the collection of commands defined
 439      * locally for style operations.
 440      *
 441      * @return the command list
 442      */
 443     public Action[] getActions() {
 444         return TextAction.augmentList(super.getActions(), defaultActions);
 445     }
 446 
 447     /**
 448      * Copies the key/values in <code>element</code>s AttributeSet into
 449      * <code>set</code>. This does not copy component, icon, or element
 450      * names attributes. Subclasses may wish to refine what is and what
 451      * isn't copied here. But be sure to first remove all the attributes that
 452      * are in <code>set</code>.<p>
 453      * This is called anytime the caret moves over a different location.
 454      *
 455      */
 456     protected void createInputAttributes(Element element,
 457                                          MutableAttributeSet set) {
 458         set.removeAttributes(set);
 459         set.addAttributes(element.getAttributes());
 460         set.removeAttribute(StyleConstants.ComposedTextAttribute);
 461 
 462         Object o = set.getAttribute(StyleConstants.NameAttribute);
 463         if (o instanceof HTML.Tag) {
 464             HTML.Tag tag = (HTML.Tag)o;
 465             // PENDING: we need a better way to express what shouldn't be
 466             // copied when editing...
 467             if(tag == HTML.Tag.IMG) {
 468                 // Remove the related image attributes, src, width, height
 469                 set.removeAttribute(HTML.Attribute.SRC);
 470                 set.removeAttribute(HTML.Attribute.HEIGHT);
 471                 set.removeAttribute(HTML.Attribute.WIDTH);
 472                 set.addAttribute(StyleConstants.NameAttribute,
 473                                  HTML.Tag.CONTENT);
 474             }
 475             else if (tag == HTML.Tag.HR || tag == HTML.Tag.BR) {
 476                 // Don't copy HRs or BRs either.
 477                 set.addAttribute(StyleConstants.NameAttribute,
 478                                  HTML.Tag.CONTENT);
 479             }
 480             else if (tag == HTML.Tag.COMMENT) {
 481                 // Don't copy COMMENTs either
 482                 set.addAttribute(StyleConstants.NameAttribute,
 483                                  HTML.Tag.CONTENT);
 484                 set.removeAttribute(HTML.Attribute.COMMENT);
 485             }
 486             else if (tag == HTML.Tag.INPUT) {
 487                 // or INPUT either
 488                 set.addAttribute(StyleConstants.NameAttribute,
 489                                  HTML.Tag.CONTENT);
 490                 set.removeAttribute(HTML.Tag.INPUT);
 491             }
 492             else if (tag instanceof HTML.UnknownTag) {
 493                 // Don't copy unknowns either:(
 494                 set.addAttribute(StyleConstants.NameAttribute,
 495                                  HTML.Tag.CONTENT);
 496                 set.removeAttribute(HTML.Attribute.ENDTAG);
 497             }
 498         }
 499     }
 500 
 501     /**
 502      * Gets the input attributes used for the styled
 503      * editing actions.
 504      *
 505      * @return the attribute set
 506      */
 507     public MutableAttributeSet getInputAttributes() {
 508         if (input == null) {
 509             input = getStyleSheet().addStyle(null, null);
 510         }
 511         return input;
 512     }
 513 
 514     /**
 515      * Sets the default cursor.
 516      *
 517      * @param cursor a cursor
 518      *
 519      * @since 1.3
 520      */
 521     public void setDefaultCursor(Cursor cursor) {
 522         defaultCursor = cursor;
 523     }
 524 
 525     /**
 526      * Returns the default cursor.
 527      *
 528      * @return the cursor
 529      *
 530      * @since 1.3
 531      */
 532     public Cursor getDefaultCursor() {
 533         return defaultCursor;
 534     }
 535 
 536     /**
 537      * Sets the cursor to use over links.
 538      *
 539      * @param cursor a cursor
 540      *
 541      * @since 1.3
 542      */
 543     public void setLinkCursor(Cursor cursor) {
 544         linkCursor = cursor;
 545     }
 546 
 547     /**
 548      * Returns the cursor to use over hyper links.
 549      *
 550      * @return the cursor
 551      *
 552      * @since 1.3
 553      */
 554     public Cursor getLinkCursor() {
 555         return linkCursor;
 556     }
 557 
 558     /**
 559      * Indicates whether an html form submission is processed automatically
 560      * or only <code>FormSubmitEvent</code> is fired.
 561      *
 562      * @return true  if html form submission is processed automatically,
 563      *         false otherwise.
 564      *
 565      * @see #setAutoFormSubmission
 566      * @since 1.5
 567      */
 568     public boolean isAutoFormSubmission() {
 569         return isAutoFormSubmission;
 570     }
 571 
 572     /**
 573      * Specifies if an html form submission is processed
 574      * automatically or only <code>FormSubmitEvent</code> is fired.
 575      * By default it is set to true.
 576      *
 577      * @param isAuto if {@code true}, html form submission is processed automatically.
 578      *
 579      * @see #isAutoFormSubmission()
 580      * @see FormSubmitEvent
 581      * @since 1.5
 582      */
 583     public void setAutoFormSubmission(boolean isAuto) {
 584         isAutoFormSubmission = isAuto;
 585     }
 586 
 587     /**
 588      * Creates a copy of the editor kit.
 589      *
 590      * @return the copy
 591      */
 592     public Object clone() {
 593         HTMLEditorKit o = (HTMLEditorKit)super.clone();
 594         if (o != null) {
 595             o.input = null;
 596             o.linkHandler = new LinkController();
 597         }
 598         return o;
 599     }
 600 
 601     /**
 602      * Fetch the parser to use for reading HTML streams.
 603      * This can be reimplemented to provide a different
 604      * parser.  The default implementation is loaded dynamically
 605      * to avoid the overhead of loading the default parser if
 606      * it's not used.  The default parser is the HotJava parser
 607      * using an HTML 3.2 DTD.
 608      *
 609      * @return the parser
 610      */
 611     protected Parser getParser() {
 612         if (defaultParser == null) {
 613             try {
 614                 Class<?> c = Class.forName("javax.swing.text.html.parser.ParserDelegator");
 615                 defaultParser = (Parser) c.newInstance();
 616             } catch (Throwable e) {
 617             }
 618         }
 619         return defaultParser;
 620     }
 621 
 622     // ----- Accessibility support -----
 623     private AccessibleContext accessibleContext;
 624 
 625     /**
 626      * returns the AccessibleContext associated with this editor kit
 627      *
 628      * @return the AccessibleContext associated with this editor kit
 629      * @since 1.4
 630      */
 631     public AccessibleContext getAccessibleContext() {
 632         if (theEditor == null) {
 633             return null;
 634         }
 635         if (accessibleContext == null) {
 636             AccessibleHTML a = new AccessibleHTML(theEditor);
 637             accessibleContext = a.getAccessibleContext();
 638         }
 639         return accessibleContext;
 640     }
 641 
 642     // --- variables ------------------------------------------
 643 
 644     private static final Cursor MoveCursor = Cursor.getPredefinedCursor
 645                                     (Cursor.HAND_CURSOR);
 646     private static final Cursor DefaultCursor = Cursor.getPredefinedCursor
 647                                     (Cursor.DEFAULT_CURSOR);
 648 
 649     /** Shared factory for creating HTML Views. */
 650     private static final ViewFactory defaultFactory = new HTMLFactory();
 651 
 652     MutableAttributeSet input;
 653     private static final Object DEFAULT_STYLES_KEY = new Object();
 654     private LinkController linkHandler = new LinkController();
 655     private static Parser defaultParser = null;
 656     private Cursor defaultCursor = DefaultCursor;
 657     private Cursor linkCursor = MoveCursor;
 658     private boolean isAutoFormSubmission = true;
 659 
 660     /**
 661      * Class to watch the associated component and fire
 662      * hyperlink events on it when appropriate.
 663      */
 664     @SuppressWarnings("serial") // Same-version serialization only
 665     public static class LinkController extends MouseAdapter implements MouseMotionListener, Serializable {
 666         private Element curElem = null;
 667         /**
 668          * If true, the current element (curElem) represents an image.
 669          */
 670         private boolean curElemImage = false;
 671         private String href = null;
 672         /** This is used by viewToModel to avoid allocing a new array each
 673          * time. */
 674         private transient Position.Bias[] bias = new Position.Bias[1];
 675         /**
 676          * Current offset.
 677          */
 678         private int curOffset;
 679 
 680         /**
 681          * Called for a mouse click event.
 682          * If the component is read-only (ie a browser) then
 683          * the clicked event is used to drive an attempt to
 684          * follow the reference specified by a link.
 685          *
 686          * @param e the mouse event
 687          * @see MouseListener#mouseClicked
 688          */
 689         public void mouseClicked(MouseEvent e) {
 690             JEditorPane editor = (JEditorPane) e.getSource();
 691 
 692             if (! editor.isEditable() && editor.isEnabled() &&
 693                     SwingUtilities.isLeftMouseButton(e)) {
 694                 Point pt = new Point(e.getX(), e.getY());
 695                 int pos = editor.viewToModel(pt);
 696                 if (pos >= 0) {
 697                     activateLink(pos, editor, e);
 698                 }
 699             }
 700         }
 701 
 702         // ignore the drags
 703         public void mouseDragged(MouseEvent e) {
 704         }
 705 
 706         // track the moving of the mouse.
 707         public void mouseMoved(MouseEvent e) {
 708             JEditorPane editor = (JEditorPane) e.getSource();
 709             if (!editor.isEnabled()) {
 710                 return;
 711             }
 712 
 713             HTMLEditorKit kit = (HTMLEditorKit)editor.getEditorKit();
 714             boolean adjustCursor = true;
 715             Cursor newCursor = kit.getDefaultCursor();
 716             if (!editor.isEditable()) {
 717                 Point pt = new Point(e.getX(), e.getY());
 718                 int pos = editor.getUI().viewToModel(editor, pt, bias);
 719                 if (bias[0] == Position.Bias.Backward && pos > 0) {
 720                     pos--;
 721                 }
 722                 if (pos >= 0 &&(editor.getDocument() instanceof HTMLDocument)){
 723                     HTMLDocument hdoc = (HTMLDocument)editor.getDocument();
 724                     Element elem = hdoc.getCharacterElement(pos);
 725                     if (!doesElementContainLocation(editor, elem, pos,
 726                                                     e.getX(), e.getY())) {
 727                         elem = null;
 728                     }
 729                     if (curElem != elem || curElemImage) {
 730                         Element lastElem = curElem;
 731                         curElem = elem;
 732                         String href = null;
 733                         curElemImage = false;
 734                         if (elem != null) {
 735                             AttributeSet a = elem.getAttributes();
 736                             AttributeSet anchor = (AttributeSet)a.
 737                                                    getAttribute(HTML.Tag.A);
 738                             if (anchor == null) {
 739                                 curElemImage = (a.getAttribute(StyleConstants.
 740                                             NameAttribute) == HTML.Tag.IMG);
 741                                 if (curElemImage) {
 742                                     href = getMapHREF(editor, hdoc, elem, a,
 743                                                       pos, e.getX(), e.getY());
 744                                 }
 745                             }
 746                             else {
 747                                 href = (String)anchor.getAttribute
 748                                     (HTML.Attribute.HREF);
 749                             }
 750                         }
 751 
 752                         if (href != this.href) {
 753                             // reference changed, fire event(s)
 754                             fireEvents(editor, hdoc, href, lastElem, e);
 755                             this.href = href;
 756                             if (href != null) {
 757                                 newCursor = kit.getLinkCursor();
 758                             }
 759                         }
 760                         else {
 761                             adjustCursor = false;
 762                         }
 763                     }
 764                     else {
 765                         adjustCursor = false;
 766                     }
 767                     curOffset = pos;
 768                 }
 769             }
 770             if (adjustCursor && editor.getCursor() != newCursor) {
 771                 editor.setCursor(newCursor);
 772             }
 773         }
 774 
 775         /**
 776          * Returns a string anchor if the passed in element has a
 777          * USEMAP that contains the passed in location.
 778          */
 779         private String getMapHREF(JEditorPane html, HTMLDocument hdoc,
 780                                   Element elem, AttributeSet attr, int offset,
 781                                   int x, int y) {
 782             Object useMap = attr.getAttribute(HTML.Attribute.USEMAP);
 783             if (useMap != null && (useMap instanceof String)) {
 784                 Map m = hdoc.getMap((String)useMap);
 785                 if (m != null && offset < hdoc.getLength()) {
 786                     Rectangle bounds;
 787                     TextUI ui = html.getUI();
 788                     try {
 789                         Shape lBounds = ui.modelToView(html, offset,
 790                                                    Position.Bias.Forward);
 791                         Shape rBounds = ui.modelToView(html, offset + 1,
 792                                                    Position.Bias.Backward);
 793                         bounds = lBounds.getBounds();
 794                         bounds.add((rBounds instanceof Rectangle) ?
 795                                     (Rectangle)rBounds : rBounds.getBounds());
 796                     } catch (BadLocationException ble) {
 797                         bounds = null;
 798                     }
 799                     if (bounds != null) {
 800                         AttributeSet area = m.getArea(x - bounds.x,
 801                                                       y - bounds.y,
 802                                                       bounds.width,
 803                                                       bounds.height);
 804                         if (area != null) {
 805                             return (String)area.getAttribute(HTML.Attribute.
 806                                                              HREF);
 807                         }
 808                     }
 809                 }
 810             }
 811             return null;
 812         }
 813 
 814         /**
 815          * Returns true if the View representing <code>e</code> contains
 816          * the location <code>x</code>, <code>y</code>. <code>offset</code>
 817          * gives the offset into the Document to check for.
 818          */
 819         private boolean doesElementContainLocation(JEditorPane editor,
 820                                                    Element e, int offset,
 821                                                    int x, int y) {
 822             if (e != null && offset > 0 && e.getStartOffset() == offset) {
 823                 try {
 824                     TextUI ui = editor.getUI();
 825                     Shape s1 = ui.modelToView(editor, offset,
 826                                               Position.Bias.Forward);
 827                     if (s1 == null) {
 828                         return false;
 829                     }
 830                     Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 :
 831                                     s1.getBounds();
 832                     Shape s2 = ui.modelToView(editor, e.getEndOffset(),
 833                                               Position.Bias.Backward);
 834                     if (s2 != null) {
 835                         Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 :
 836                                     s2.getBounds();
 837                         r1.add(r2);
 838                     }
 839                     return r1.contains(x, y);
 840                 } catch (BadLocationException ble) {
 841                 }
 842             }
 843             return true;
 844         }
 845 
 846         /**
 847          * Calls linkActivated on the associated JEditorPane
 848          * if the given position represents a link.<p>This is implemented
 849          * to forward to the method with the same name, but with the following
 850          * args both == -1.
 851          *
 852          * @param pos the position
 853          * @param editor the editor pane
 854          */
 855         protected void activateLink(int pos, JEditorPane editor) {
 856             activateLink(pos, editor, null);
 857         }
 858 
 859         /**
 860          * Calls linkActivated on the associated JEditorPane
 861          * if the given position represents a link. If this was the result
 862          * of a mouse click, <code>x</code> and
 863          * <code>y</code> will give the location of the mouse, otherwise
 864          * they will be {@literal <} 0.
 865          *
 866          * @param pos the position
 867          * @param html the editor pane
 868          */
 869         void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) {
 870             Document doc = html.getDocument();
 871             if (doc instanceof HTMLDocument) {
 872                 HTMLDocument hdoc = (HTMLDocument) doc;
 873                 Element e = hdoc.getCharacterElement(pos);
 874                 AttributeSet a = e.getAttributes();
 875                 AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
 876                 HyperlinkEvent linkEvent = null;
 877                 String description;
 878                 int x = -1;
 879                 int y = -1;
 880 
 881                 if (mouseEvent != null) {
 882                     x = mouseEvent.getX();
 883                     y = mouseEvent.getY();
 884                 }
 885 
 886                 if (anchor == null) {
 887                     href = getMapHREF(html, hdoc, e, a, pos, x, y);
 888                 }
 889                 else {
 890                     href = (String)anchor.getAttribute(HTML.Attribute.HREF);
 891                 }
 892 
 893                 if (href != null) {
 894                     linkEvent = createHyperlinkEvent(html, hdoc, href, anchor,
 895                                                      e, mouseEvent);
 896                 }
 897                 if (linkEvent != null) {
 898                     html.fireHyperlinkUpdate(linkEvent);
 899                 }
 900             }
 901         }
 902 
 903         /**
 904          * Creates and returns a new instance of HyperlinkEvent. If
 905          * <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent
 906          * will be created.
 907          */
 908         HyperlinkEvent createHyperlinkEvent(JEditorPane html,
 909                                             HTMLDocument hdoc, String href,
 910                                             AttributeSet anchor,
 911                                             Element element,
 912                                             MouseEvent mouseEvent) {
 913             URL u;
 914             try {
 915                 URL base = hdoc.getBase();
 916                 u = new URL(base, href);
 917                 // Following is a workaround for 1.2, in which
 918                 // new URL("file://...", "#...") causes the filename to
 919                 // be lost.
 920                 if (href != null && "file".equals(u.getProtocol()) &&
 921                     href.startsWith("#")) {
 922                     String baseFile = base.getFile();
 923                     String newFile = u.getFile();
 924                     if (baseFile != null && newFile != null &&
 925                         !newFile.startsWith(baseFile)) {
 926                         u = new URL(base, baseFile + href);
 927                     }
 928                 }
 929             } catch (MalformedURLException m) {
 930                 u = null;
 931             }
 932             HyperlinkEvent linkEvent;
 933 
 934             if (!hdoc.isFrameDocument()) {
 935                 linkEvent = new HyperlinkEvent(
 936                         html, HyperlinkEvent.EventType.ACTIVATED, u, href,
 937                         element, mouseEvent);
 938             } else {
 939                 String target = (anchor != null) ?
 940                     (String)anchor.getAttribute(HTML.Attribute.TARGET) : null;
 941                 if ((target == null) || (target.equals(""))) {
 942                     target = hdoc.getBaseTarget();
 943                 }
 944                 if ((target == null) || (target.equals(""))) {
 945                     target = "_self";
 946                 }
 947                     linkEvent = new HTMLFrameHyperlinkEvent(
 948                         html, HyperlinkEvent.EventType.ACTIVATED, u, href,
 949                         element, mouseEvent, target);
 950             }
 951             return linkEvent;
 952         }
 953 
 954         void fireEvents(JEditorPane editor, HTMLDocument doc, String href,
 955                         Element lastElem, MouseEvent mouseEvent) {
 956             if (this.href != null) {
 957                 // fire an exited event on the old link
 958                 URL u;
 959                 try {
 960                     u = new URL(doc.getBase(), this.href);
 961                 } catch (MalformedURLException m) {
 962                     u = null;
 963                 }
 964                 HyperlinkEvent exit = new HyperlinkEvent(editor,
 965                                  HyperlinkEvent.EventType.EXITED, u, this.href,
 966                                  lastElem, mouseEvent);
 967                 editor.fireHyperlinkUpdate(exit);
 968             }
 969             if (href != null) {
 970                 // fire an entered event on the new link
 971                 URL u;
 972                 try {
 973                     u = new URL(doc.getBase(), href);
 974                 } catch (MalformedURLException m) {
 975                     u = null;
 976                 }
 977                 HyperlinkEvent entered = new HyperlinkEvent(editor,
 978                                             HyperlinkEvent.EventType.ENTERED,
 979                                             u, href, curElem, mouseEvent);
 980                 editor.fireHyperlinkUpdate(entered);
 981             }
 982         }
 983     }
 984 
 985     /**
 986      * Interface to be supported by the parser.  This enables
 987      * providing a different parser while reusing some of the
 988      * implementation provided by this editor kit.
 989      */
 990     public abstract static class Parser {
 991         /**
 992          * Parse the given stream and drive the given callback
 993          * with the results of the parse.  This method should
 994          * be implemented to be thread-safe.
 995          *
 996          * @param r a reader
 997          * @param cb a parser callback
 998          * @param ignoreCharSet if {@code true} charset is ignoring
 999          * @throws IOException if an I/O exception occurs
1000          */
1001         public abstract void parse(Reader r, ParserCallback cb, boolean ignoreCharSet) throws IOException;
1002 
1003     }
1004 
1005     /**
1006      * The result of parsing drives these callback methods.
1007      * The open and close actions should be balanced.  The
1008      * <code>flush</code> method will be the last method
1009      * called, to give the receiver a chance to flush any
1010      * pending data into the document.
1011      * <p>Refer to DocumentParser, the default parser used, for further
1012      * information on the contents of the AttributeSets, the positions, and
1013      * other info.
1014      *
1015      * @see javax.swing.text.html.parser.DocumentParser
1016      */
1017     public static class ParserCallback {
1018         /**
1019          * This is passed as an attribute in the attributeset to indicate
1020          * the element is implied eg, the string '&lt;&gt;foo&lt;\t&gt;'
1021          * contains an implied html element and an implied body element.
1022          *
1023          * @since 1.3
1024          */
1025         public static final Object IMPLIED = "_implied_";
1026 
1027         /**
1028          * The last method called on the reader. It allows
1029          * any pending changes to be flushed into the document.
1030          * Since this is currently loading synchronously, the entire
1031          * set of changes are pushed in at this point.
1032          *
1033          * @throws BadLocationException if the given position does not
1034          *   represent a valid location in the associated document.
1035          */
1036         public void flush() throws BadLocationException {
1037         }
1038 
1039         /**
1040          * Called by the parser to indicate a block of text was
1041          * encountered.
1042          *
1043          * @param data a data
1044          * @param pos a position
1045          */
1046         public void handleText(char[] data, int pos) {
1047         }
1048 
1049         /**
1050          * Called by the parser to indicate a block of comment was
1051          * encountered.
1052          *
1053          * @param data a data
1054          * @param pos a position
1055          */
1056         public void handleComment(char[] data, int pos) {
1057         }
1058 
1059         /**
1060          * Callback from the parser. Route to the appropriate
1061          * handler for the tag.
1062          *
1063          * @param t an HTML tag
1064          * @param a a set of attributes
1065          * @param pos a position
1066          */
1067         public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
1068         }
1069 
1070         /**
1071          * Callback from the parser. Route to the appropriate
1072          * handler for the tag.
1073          *
1074          * @param t an HTML tag
1075          * @param pos a position
1076          */
1077         public void handleEndTag(HTML.Tag t, int pos) {
1078         }
1079 
1080         /**
1081          * Callback from the parser. Route to the appropriate
1082          * handler for the tag.
1083          *
1084          * @param t an HTML tag
1085          * @param a a set of attributes
1086          * @param pos a position
1087          */
1088         public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
1089         }
1090 
1091         /**
1092          * Callback from the parser. Route to the appropriate
1093          * handler for the error.
1094          *
1095          * @param errorMsg a error message
1096          * @param pos a position
1097          */
1098         public void handleError(String errorMsg, int pos) {
1099         }
1100 
1101         /**
1102          * This is invoked after the stream has been parsed, but before
1103          * <code>flush</code>. <code>eol</code> will be one of \n, \r
1104          * or \r\n, which ever is encountered the most in parsing the
1105          * stream.
1106          *
1107          * @param eol value of eol
1108          *
1109          * @since 1.3
1110          */
1111         public void handleEndOfLineString(String eol) {
1112         }
1113     }
1114 
1115     /**
1116      * A factory to build views for HTML.  The following
1117      * table describes what this factory will build by
1118      * default.
1119      *
1120      * <table summary="Describes the tag and view created by this factory by default">
1121      * <tr>
1122      * <th align=left>Tag<th align=left>View created
1123      * </tr><tr>
1124      * <td>HTML.Tag.CONTENT<td>InlineView
1125      * </tr><tr>
1126      * <td>HTML.Tag.IMPLIED<td>javax.swing.text.html.ParagraphView
1127      * </tr><tr>
1128      * <td>HTML.Tag.P<td>javax.swing.text.html.ParagraphView
1129      * </tr><tr>
1130      * <td>HTML.Tag.H1<td>javax.swing.text.html.ParagraphView
1131      * </tr><tr>
1132      * <td>HTML.Tag.H2<td>javax.swing.text.html.ParagraphView
1133      * </tr><tr>
1134      * <td>HTML.Tag.H3<td>javax.swing.text.html.ParagraphView
1135      * </tr><tr>
1136      * <td>HTML.Tag.H4<td>javax.swing.text.html.ParagraphView
1137      * </tr><tr>
1138      * <td>HTML.Tag.H5<td>javax.swing.text.html.ParagraphView
1139      * </tr><tr>
1140      * <td>HTML.Tag.H6<td>javax.swing.text.html.ParagraphView
1141      * </tr><tr>
1142      * <td>HTML.Tag.DT<td>javax.swing.text.html.ParagraphView
1143      * </tr><tr>
1144      * <td>HTML.Tag.MENU<td>ListView
1145      * </tr><tr>
1146      * <td>HTML.Tag.DIR<td>ListView
1147      * </tr><tr>
1148      * <td>HTML.Tag.UL<td>ListView
1149      * </tr><tr>
1150      * <td>HTML.Tag.OL<td>ListView
1151      * </tr><tr>
1152      * <td>HTML.Tag.LI<td>BlockView
1153      * </tr><tr>
1154      * <td>HTML.Tag.DL<td>BlockView
1155      * </tr><tr>
1156      * <td>HTML.Tag.DD<td>BlockView
1157      * </tr><tr>
1158      * <td>HTML.Tag.BODY<td>BlockView
1159      * </tr><tr>
1160      * <td>HTML.Tag.HTML<td>BlockView
1161      * </tr><tr>
1162      * <td>HTML.Tag.CENTER<td>BlockView
1163      * </tr><tr>
1164      * <td>HTML.Tag.DIV<td>BlockView
1165      * </tr><tr>
1166      * <td>HTML.Tag.BLOCKQUOTE<td>BlockView
1167      * </tr><tr>
1168      * <td>HTML.Tag.PRE<td>BlockView
1169      * </tr><tr>
1170      * <td>HTML.Tag.BLOCKQUOTE<td>BlockView
1171      * </tr><tr>
1172      * <td>HTML.Tag.PRE<td>BlockView
1173      * </tr><tr>
1174      * <td>HTML.Tag.IMG<td>ImageView
1175      * </tr><tr>
1176      * <td>HTML.Tag.HR<td>HRuleView
1177      * </tr><tr>
1178      * <td>HTML.Tag.BR<td>BRView
1179      * </tr><tr>
1180      * <td>HTML.Tag.TABLE<td>javax.swing.text.html.TableView
1181      * </tr><tr>
1182      * <td>HTML.Tag.INPUT<td>FormView
1183      * </tr><tr>
1184      * <td>HTML.Tag.SELECT<td>FormView
1185      * </tr><tr>
1186      * <td>HTML.Tag.TEXTAREA<td>FormView
1187      * </tr><tr>
1188      * <td>HTML.Tag.OBJECT<td>ObjectView
1189      * </tr><tr>
1190      * <td>HTML.Tag.FRAMESET<td>FrameSetView
1191      * </tr><tr>
1192      * <td>HTML.Tag.FRAME<td>FrameView
1193      * </tr>
1194      * </table>
1195      */
1196     public static class HTMLFactory implements ViewFactory {
1197 
1198         /**
1199          * Creates a view from an element.
1200          *
1201          * @param elem the element
1202          * @return the view
1203          */
1204         public View create(Element elem) {
1205             AttributeSet attrs = elem.getAttributes();
1206             Object elementName =
1207                 attrs.getAttribute(AbstractDocument.ElementNameAttribute);
1208             Object o = (elementName != null) ?
1209                 null : attrs.getAttribute(StyleConstants.NameAttribute);
1210             if (o instanceof HTML.Tag) {
1211                 HTML.Tag kind = (HTML.Tag) o;
1212                 if (kind == HTML.Tag.CONTENT) {
1213                     return new InlineView(elem);
1214                 } else if (kind == HTML.Tag.IMPLIED) {
1215                     String ws = (String) elem.getAttributes().getAttribute(
1216                         CSS.Attribute.WHITE_SPACE);
1217                     if ((ws != null) && ws.equals("pre")) {
1218                         return new LineView(elem);
1219                     }
1220                     return new javax.swing.text.html.ParagraphView(elem);
1221                 } else if ((kind == HTML.Tag.P) ||
1222                            (kind == HTML.Tag.H1) ||
1223                            (kind == HTML.Tag.H2) ||
1224                            (kind == HTML.Tag.H3) ||
1225                            (kind == HTML.Tag.H4) ||
1226                            (kind == HTML.Tag.H5) ||
1227                            (kind == HTML.Tag.H6) ||
1228                            (kind == HTML.Tag.DT)) {
1229                     // paragraph
1230                     return new javax.swing.text.html.ParagraphView(elem);
1231                 } else if ((kind == HTML.Tag.MENU) ||
1232                            (kind == HTML.Tag.DIR) ||
1233                            (kind == HTML.Tag.UL)   ||
1234                            (kind == HTML.Tag.OL)) {
1235                     return new ListView(elem);
1236                 } else if (kind == HTML.Tag.BODY) {
1237                     return new BodyBlockView(elem);
1238                 } else if (kind == HTML.Tag.HTML) {
1239                     return new BlockView(elem, View.Y_AXIS);
1240                 } else if ((kind == HTML.Tag.LI) ||
1241                            (kind == HTML.Tag.CENTER) ||
1242                            (kind == HTML.Tag.DL) ||
1243                            (kind == HTML.Tag.DD) ||
1244                            (kind == HTML.Tag.DIV) ||
1245                            (kind == HTML.Tag.BLOCKQUOTE) ||
1246                            (kind == HTML.Tag.PRE) ||
1247                            (kind == HTML.Tag.FORM)) {
1248                     // vertical box
1249                     return new BlockView(elem, View.Y_AXIS);
1250                 } else if (kind == HTML.Tag.NOFRAMES) {
1251                     return new NoFramesView(elem, View.Y_AXIS);
1252                 } else if (kind==HTML.Tag.IMG) {
1253                     return new ImageView(elem);
1254                 } else if (kind == HTML.Tag.ISINDEX) {
1255                     return new IsindexView(elem);
1256                 } else if (kind == HTML.Tag.HR) {
1257                     return new HRuleView(elem);
1258                 } else if (kind == HTML.Tag.BR) {
1259                     return new BRView(elem);
1260                 } else if (kind == HTML.Tag.TABLE) {
1261                     return new javax.swing.text.html.TableView(elem);
1262                 } else if ((kind == HTML.Tag.INPUT) ||
1263                            (kind == HTML.Tag.SELECT) ||
1264                            (kind == HTML.Tag.TEXTAREA)) {
1265                     return new FormView(elem);
1266                 } else if (kind == HTML.Tag.OBJECT) {
1267                     return new ObjectView(elem);
1268                 } else if (kind == HTML.Tag.FRAMESET) {
1269                      if (elem.getAttributes().isDefined(HTML.Attribute.ROWS)) {
1270                          return new FrameSetView(elem, View.Y_AXIS);
1271                      } else if (elem.getAttributes().isDefined(HTML.Attribute.COLS)) {
1272                          return new FrameSetView(elem, View.X_AXIS);
1273                      }
1274                      throw new RuntimeException("Can't build a"  + kind + ", " + elem + ":" +
1275                                      "no ROWS or COLS defined.");
1276                 } else if (kind == HTML.Tag.FRAME) {
1277                     return new FrameView(elem);
1278                 } else if (kind instanceof HTML.UnknownTag) {
1279                     return new HiddenTagView(elem);
1280                 } else if (kind == HTML.Tag.COMMENT) {
1281                     return new CommentView(elem);
1282                 } else if (kind == HTML.Tag.HEAD) {
1283                     // Make the head never visible, and never load its
1284                     // children. For Cursor positioning,
1285                     // getNextVisualPositionFrom is overriden to always return
1286                     // the end offset of the element.
1287                     return new BlockView(elem, View.X_AXIS) {
1288                         public float getPreferredSpan(int axis) {
1289                             return 0;
1290                         }
1291                         public float getMinimumSpan(int axis) {
1292                             return 0;
1293                         }
1294                         public float getMaximumSpan(int axis) {
1295                             return 0;
1296                         }
1297                         protected void loadChildren(ViewFactory f) {
1298                         }
1299                         public Shape modelToView(int pos, Shape a,
1300                                Position.Bias b) throws BadLocationException {
1301                             return a;
1302                         }
1303                         public int getNextVisualPositionFrom(int pos,
1304                                      Position.Bias b, Shape a,
1305                                      int direction, Position.Bias[] biasRet) {
1306                             return getElement().getEndOffset();
1307                         }
1308                     };
1309                 } else if ((kind == HTML.Tag.TITLE) ||
1310                            (kind == HTML.Tag.META) ||
1311                            (kind == HTML.Tag.LINK) ||
1312                            (kind == HTML.Tag.STYLE) ||
1313                            (kind == HTML.Tag.SCRIPT) ||
1314                            (kind == HTML.Tag.AREA) ||
1315                            (kind == HTML.Tag.MAP) ||
1316                            (kind == HTML.Tag.PARAM) ||
1317                            (kind == HTML.Tag.APPLET)) {
1318                     return new HiddenTagView(elem);
1319                 }
1320             }
1321             // If we get here, it's either an element we don't know about
1322             // or something from StyledDocument that doesn't have a mapping to HTML.
1323             String nm = (elementName != null) ? (String)elementName :
1324                                                 elem.getName();
1325             if (nm != null) {
1326                 if (nm.equals(AbstractDocument.ContentElementName)) {
1327                     return new LabelView(elem);
1328                 } else if (nm.equals(AbstractDocument.ParagraphElementName)) {
1329                     return new ParagraphView(elem);
1330                 } else if (nm.equals(AbstractDocument.SectionElementName)) {
1331                     return new BoxView(elem, View.Y_AXIS);
1332                 } else if (nm.equals(StyleConstants.ComponentElementName)) {
1333                     return new ComponentView(elem);
1334                 } else if (nm.equals(StyleConstants.IconElementName)) {
1335                     return new IconView(elem);
1336                 }
1337             }
1338 
1339             // default to text display
1340             return new LabelView(elem);
1341         }
1342 
1343         static class BodyBlockView extends BlockView implements ComponentListener {
1344             public BodyBlockView(Element elem) {
1345                 super(elem,View.Y_AXIS);
1346             }
1347             // reimplement major axis requirements to indicate that the
1348             // block is flexible for the body element... so that it can
1349             // be stretched to fill the background properly.
1350             protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) {
1351                 r = super.calculateMajorAxisRequirements(axis, r);
1352                 r.maximum = Integer.MAX_VALUE;
1353                 return r;
1354             }
1355 
1356             protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
1357                 Container container = getContainer();
1358                 Container parentContainer;
1359                 if (container != null
1360                     && (container instanceof javax.swing.JEditorPane)
1361                     && (parentContainer = container.getParent()) != null
1362                     && (parentContainer instanceof javax.swing.JViewport)) {
1363                     JViewport viewPort = (JViewport)parentContainer;
1364                     if (cachedViewPort != null) {
1365                         JViewport cachedObject = cachedViewPort.get();
1366                         if (cachedObject != null) {
1367                             if (cachedObject != viewPort) {
1368                                 cachedObject.removeComponentListener(this);
1369                             }
1370                         } else {
1371                             cachedViewPort = null;
1372                         }
1373                     }
1374                     if (cachedViewPort == null) {
1375                         viewPort.addComponentListener(this);
1376                         cachedViewPort = new WeakReference<JViewport>(viewPort);
1377                     }
1378 
1379                     componentVisibleWidth = viewPort.getExtentSize().width;
1380                     if (componentVisibleWidth > 0) {
1381                     Insets insets = container.getInsets();
1382                     viewVisibleWidth = componentVisibleWidth - insets.left - getLeftInset();
1383                     //try to use viewVisibleWidth if it is smaller than targetSpan
1384                     targetSpan = Math.min(targetSpan, viewVisibleWidth);
1385                     }
1386                 } else {
1387                     if (cachedViewPort != null) {
1388                         JViewport cachedObject = cachedViewPort.get();
1389                         if (cachedObject != null) {
1390                             cachedObject.removeComponentListener(this);
1391                         }
1392                         cachedViewPort = null;
1393                     }
1394                 }
1395                 super.layoutMinorAxis(targetSpan, axis, offsets, spans);
1396             }
1397 
1398             public void setParent(View parent) {
1399                 //if parent == null unregister component listener
1400                 if (parent == null) {
1401                     if (cachedViewPort != null) {
1402                         Object cachedObject;
1403                         if ((cachedObject = cachedViewPort.get()) != null) {
1404                             ((JComponent)cachedObject).removeComponentListener(this);
1405                         }
1406                         cachedViewPort = null;
1407                     }
1408                 }
1409                 super.setParent(parent);
1410             }
1411 
1412             public void componentResized(ComponentEvent e) {
1413                 if ( !(e.getSource() instanceof JViewport) ) {
1414                     return;
1415                 }
1416                 JViewport viewPort = (JViewport)e.getSource();
1417                 if (componentVisibleWidth != viewPort.getExtentSize().width) {
1418                     Document doc = getDocument();
1419                     if (doc instanceof AbstractDocument) {
1420                         AbstractDocument document = (AbstractDocument)getDocument();
1421                         document.readLock();
1422                         try {
1423                             layoutChanged(X_AXIS);
1424                             preferenceChanged(null, true, true);
1425                         } finally {
1426                             document.readUnlock();
1427                         }
1428 
1429                     }
1430                 }
1431             }
1432             public void componentHidden(ComponentEvent e) {
1433             }
1434             public void componentMoved(ComponentEvent e) {
1435             }
1436             public void componentShown(ComponentEvent e) {
1437             }
1438             /*
1439              * we keep weak reference to viewPort if and only if BodyBoxView is listening for ComponentEvents
1440              * only in that case cachedViewPort is not equal to null.
1441              * we need to keep this reference in order to remove BodyBoxView from viewPort listeners.
1442              *
1443              */
1444             private Reference<JViewport> cachedViewPort = null;
1445             private boolean isListening = false;
1446             private int viewVisibleWidth = Integer.MAX_VALUE;
1447             private int componentVisibleWidth = Integer.MAX_VALUE;
1448         }
1449 
1450     }
1451 
1452     // --- Action implementations ------------------------------
1453 
1454 /** The bold action identifier
1455 */
1456     public static final String  BOLD_ACTION = "html-bold-action";
1457 /** The italic action identifier
1458 */
1459     public static final String  ITALIC_ACTION = "html-italic-action";
1460 /** The paragraph left indent action identifier
1461 */
1462     public static final String  PARA_INDENT_LEFT = "html-para-indent-left";
1463 /** The paragraph right indent action identifier
1464 */
1465     public static final String  PARA_INDENT_RIGHT = "html-para-indent-right";
1466 /** The  font size increase to next value action identifier
1467 */
1468     public static final String  FONT_CHANGE_BIGGER = "html-font-bigger";
1469 /** The font size decrease to next value action identifier
1470 */
1471     public static final String  FONT_CHANGE_SMALLER = "html-font-smaller";
1472 /** The Color choice action identifier
1473      The color is passed as an argument
1474 */
1475     public static final String  COLOR_ACTION = "html-color-action";
1476 /** The logical style choice action identifier
1477      The logical style is passed in as an argument
1478 */
1479     public static final String  LOGICAL_STYLE_ACTION = "html-logical-style-action";
1480     /**
1481      * Align images at the top.
1482      */
1483     public static final String  IMG_ALIGN_TOP = "html-image-align-top";
1484 
1485     /**
1486      * Align images in the middle.
1487      */
1488     public static final String  IMG_ALIGN_MIDDLE = "html-image-align-middle";
1489 
1490     /**
1491      * Align images at the bottom.
1492      */
1493     public static final String  IMG_ALIGN_BOTTOM = "html-image-align-bottom";
1494 
1495     /**
1496      * Align images at the border.
1497      */
1498     public static final String  IMG_BORDER = "html-image-border";
1499 
1500 
1501     /** HTML used when inserting tables. */
1502     private static final String INSERT_TABLE_HTML = "<table border=1><tr><td></td></tr></table>";
1503 
1504     /** HTML used when inserting unordered lists. */
1505     private static final String INSERT_UL_HTML = "<ul><li></li></ul>";
1506 
1507     /** HTML used when inserting ordered lists. */
1508     private static final String INSERT_OL_HTML = "<ol><li></li></ol>";
1509 
1510     /** HTML used when inserting hr. */
1511     private static final String INSERT_HR_HTML = "<hr>";
1512 
1513     /** HTML used when inserting pre. */
1514     private static final String INSERT_PRE_HTML = "<pre></pre>";
1515 
1516     private static final NavigateLinkAction nextLinkAction =
1517         new NavigateLinkAction("next-link-action");
1518 
1519     private static final NavigateLinkAction previousLinkAction =
1520         new NavigateLinkAction("previous-link-action");
1521 
1522     private static final ActivateLinkAction activateLinkAction =
1523         new ActivateLinkAction("activate-link-action");
1524 
1525     private static final Action[] defaultActions = {
1526         new InsertHTMLTextAction("InsertTable", INSERT_TABLE_HTML,
1527                                  HTML.Tag.BODY, HTML.Tag.TABLE),
1528         new InsertHTMLTextAction("InsertTableRow", INSERT_TABLE_HTML,
1529                                  HTML.Tag.TABLE, HTML.Tag.TR,
1530                                  HTML.Tag.BODY, HTML.Tag.TABLE),
1531         new InsertHTMLTextAction("InsertTableDataCell", INSERT_TABLE_HTML,
1532                                  HTML.Tag.TR, HTML.Tag.TD,
1533                                  HTML.Tag.BODY, HTML.Tag.TABLE),
1534         new InsertHTMLTextAction("InsertUnorderedList", INSERT_UL_HTML,
1535                                  HTML.Tag.BODY, HTML.Tag.UL),
1536         new InsertHTMLTextAction("InsertUnorderedListItem", INSERT_UL_HTML,
1537                                  HTML.Tag.UL, HTML.Tag.LI,
1538                                  HTML.Tag.BODY, HTML.Tag.UL),
1539         new InsertHTMLTextAction("InsertOrderedList", INSERT_OL_HTML,
1540                                  HTML.Tag.BODY, HTML.Tag.OL),
1541         new InsertHTMLTextAction("InsertOrderedListItem", INSERT_OL_HTML,
1542                                  HTML.Tag.OL, HTML.Tag.LI,
1543                                  HTML.Tag.BODY, HTML.Tag.OL),
1544         new InsertHRAction(),
1545         new InsertHTMLTextAction("InsertPre", INSERT_PRE_HTML,
1546                                  HTML.Tag.BODY, HTML.Tag.PRE),
1547         nextLinkAction, previousLinkAction, activateLinkAction,
1548 
1549         new BeginAction(beginAction, false),
1550         new BeginAction(selectionBeginAction, true)
1551     };
1552 
1553     // link navigation support
1554     private boolean foundLink = false;
1555     private int prevHypertextOffset = -1;
1556     private Object linkNavigationTag;
1557 
1558 
1559     /**
1560      * An abstract Action providing some convenience methods that may
1561      * be useful in inserting HTML into an existing document.
1562      * <p>NOTE: None of the convenience methods obtain a lock on the
1563      * document. If you have another thread modifying the text these
1564      * methods may have inconsistent behavior, or return the wrong thing.
1565      */
1566     @SuppressWarnings("serial") // Superclass is not serializable across versions
1567     public abstract static class HTMLTextAction extends StyledTextAction {
1568 
1569         /**
1570          * Creates a new HTMLTextAction from a string action name.
1571          *
1572          * @param name the name of the action
1573          */
1574         public HTMLTextAction(String name) {
1575             super(name);
1576         }
1577 
1578         /**
1579          * @param e the JEditorPane
1580          * @return HTMLDocument of <code>e</code>.
1581          */
1582         protected HTMLDocument getHTMLDocument(JEditorPane e) {
1583             Document d = e.getDocument();
1584             if (d instanceof HTMLDocument) {
1585                 return (HTMLDocument) d;
1586             }
1587             throw new IllegalArgumentException("document must be HTMLDocument");
1588         }
1589 
1590         /**
1591          * @param e the JEditorPane
1592          * @return HTMLEditorKit for <code>e</code>.
1593          */
1594         protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) {
1595             EditorKit k = e.getEditorKit();
1596             if (k instanceof HTMLEditorKit) {
1597                 return (HTMLEditorKit) k;
1598             }
1599             throw new IllegalArgumentException("EditorKit must be HTMLEditorKit");
1600         }
1601 
1602         /**
1603          * Returns an array of the Elements that contain <code>offset</code>.
1604          * The first elements corresponds to the root.
1605          *
1606          * @param doc an instance of HTMLDocument
1607          * @param offset value of offset
1608          * @return an array of the Elements that contain <code>offset</code>
1609          */
1610         protected Element[] getElementsAt(HTMLDocument doc, int offset) {
1611             return getElementsAt(doc.getDefaultRootElement(), offset, 0);
1612         }
1613 
1614         /**
1615          * Recursive method used by getElementsAt.
1616          */
1617         private Element[] getElementsAt(Element parent, int offset,
1618                                         int depth) {
1619             if (parent.isLeaf()) {
1620                 Element[] retValue = new Element[depth + 1];
1621                 retValue[depth] = parent;
1622                 return retValue;
1623             }
1624             Element[] retValue = getElementsAt(parent.getElement
1625                           (parent.getElementIndex(offset)), offset, depth + 1);
1626             retValue[depth] = parent;
1627             return retValue;
1628         }
1629 
1630         /**
1631          * Returns number of elements, starting at the deepest leaf, needed
1632          * to get to an element representing <code>tag</code>. This will
1633          * return -1 if no elements is found representing <code>tag</code>,
1634          * or 0 if the parent of the leaf at <code>offset</code> represents
1635          * <code>tag</code>.
1636          *
1637          * @param doc an instance of HTMLDocument
1638          * @param offset an offset to start from
1639          * @param tag tag to represent
1640          * @return number of elements
1641          */
1642         protected int elementCountToTag(HTMLDocument doc, int offset,
1643                                         HTML.Tag tag) {
1644             int depth = -1;
1645             Element e = doc.getCharacterElement(offset);
1646             while (e != null && e.getAttributes().getAttribute
1647                    (StyleConstants.NameAttribute) != tag) {
1648                 e = e.getParentElement();
1649                 depth++;
1650             }
1651             if (e == null) {
1652                 return -1;
1653             }
1654             return depth;
1655         }
1656 
1657         /**
1658          * Returns the deepest element at <code>offset</code> matching
1659          * <code>tag</code>.
1660          *
1661          * @param doc an instance of HTMLDocument
1662          * @param offset the specified offset &gt;= 0
1663          * @param tag an instance of HTML.Tag
1664          *
1665          * @return the deepest element
1666          */
1667         protected Element findElementMatchingTag(HTMLDocument doc, int offset,
1668                                                  HTML.Tag tag) {
1669             Element e = doc.getDefaultRootElement();
1670             Element lastMatch = null;
1671             while (e != null) {
1672                 if (e.getAttributes().getAttribute
1673                    (StyleConstants.NameAttribute) == tag) {
1674                     lastMatch = e;
1675                 }
1676                 e = e.getElement(e.getElementIndex(offset));
1677             }
1678             return lastMatch;
1679         }
1680     }
1681 
1682 
1683     /**
1684      * InsertHTMLTextAction can be used to insert an arbitrary string of HTML
1685      * into an existing HTML document. At least two HTML.Tags need to be
1686      * supplied. The first Tag, parentTag, identifies the parent in
1687      * the document to add the elements to. The second tag, addTag,
1688      * identifies the first tag that should be added to the document as
1689      * seen in the HTML string. One important thing to remember, is that
1690      * the parser is going to generate all the appropriate tags, even if
1691      * they aren't in the HTML string passed in.<p>
1692      * For example, lets say you wanted to create an action to insert
1693      * a table into the body. The parentTag would be HTML.Tag.BODY,
1694      * addTag would be HTML.Tag.TABLE, and the string could be something
1695      * like &lt;table&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;.
1696      * <p>There is also an option to supply an alternate parentTag and
1697      * addTag. These will be checked for if there is no parentTag at
1698      * offset.
1699      */
1700     @SuppressWarnings("serial") // Superclass is not serializable across versions
1701     public static class InsertHTMLTextAction extends HTMLTextAction {
1702 
1703         /**
1704          * Creates a new InsertHTMLTextAction.
1705          *
1706          * @param name a name of the action
1707          * @param html an HTML string
1708          * @param parentTag a parent tag
1709          * @param addTag the first tag to start inserting into document
1710          */
1711         public InsertHTMLTextAction(String name, String html,
1712                                     HTML.Tag parentTag, HTML.Tag addTag) {
1713             this(name, html, parentTag, addTag, null, null);
1714         }
1715 
1716         /**
1717          * Creates a new InsertHTMLTextAction.
1718          *
1719          * @param name a name of the action
1720          * @param html an HTML string
1721          * @param parentTag a parent tag
1722          * @param addTag the first tag to start inserting into document
1723          * @param alternateParentTag an alternative parent tag
1724          * @param alternateAddTag an alternative tag
1725          */
1726         public InsertHTMLTextAction(String name, String html,
1727                                     HTML.Tag parentTag,
1728                                     HTML.Tag addTag,
1729                                     HTML.Tag alternateParentTag,
1730                                     HTML.Tag alternateAddTag) {
1731             this(name, html, parentTag, addTag, alternateParentTag,
1732                  alternateAddTag, true);
1733         }
1734 
1735         /* public */
1736         InsertHTMLTextAction(String name, String html,
1737                                     HTML.Tag parentTag,
1738                                     HTML.Tag addTag,
1739                                     HTML.Tag alternateParentTag,
1740                                     HTML.Tag alternateAddTag,
1741                                     boolean adjustSelection) {
1742             super(name);
1743             this.html = html;
1744             this.parentTag = parentTag;
1745             this.addTag = addTag;
1746             this.alternateParentTag = alternateParentTag;
1747             this.alternateAddTag = alternateAddTag;
1748             this.adjustSelection = adjustSelection;
1749         }
1750 
1751         /**
1752          * A cover for HTMLEditorKit.insertHTML. If an exception it
1753          * thrown it is wrapped in a RuntimeException and thrown.
1754          *
1755          * @param editor an instance of JEditorPane
1756          * @param doc the document to insert into
1757          * @param offset the offset to insert HTML at
1758          * @param html an HTML string
1759          * @param popDepth the number of ElementSpec.EndTagTypes to generate
1760          *                  before inserting
1761          * @param pushDepth the number of ElementSpec.StartTagTypes with a direction
1762          *                  of ElementSpec.JoinNextDirection that should be generated
1763          *                  before inserting, but after the end tags have been generated
1764          * @param addTag the first tag to start inserting into document
1765          */
1766         protected void insertHTML(JEditorPane editor, HTMLDocument doc,
1767                                   int offset, String html, int popDepth,
1768                                   int pushDepth, HTML.Tag addTag) {
1769             try {
1770                 getHTMLEditorKit(editor).insertHTML(doc, offset, html,
1771                                                     popDepth, pushDepth,
1772                                                     addTag);
1773             } catch (IOException ioe) {
1774                 throw new RuntimeException("Unable to insert: " + ioe);
1775             } catch (BadLocationException ble) {
1776                 throw new RuntimeException("Unable to insert: " + ble);
1777             }
1778         }
1779 
1780         /**
1781          * This is invoked when inserting at a boundary. It determines
1782          * the number of pops, and then the number of pushes that need
1783          * to be performed, and then invokes insertHTML.
1784          *
1785          * @param editor an instance of JEditorPane
1786          * @param doc an instance of HTMLDocument
1787          * @param offset an offset to start from
1788          * @param insertElement an instance of Element
1789          * @param html an HTML string
1790          * @param parentTag a parent tag
1791          * @param addTag the first tag to start inserting into document
1792          *
1793          * @since 1.3
1794          *
1795          */
1796         protected void insertAtBoundary(JEditorPane editor, HTMLDocument doc,
1797                                         int offset, Element insertElement,
1798                                         String html, HTML.Tag parentTag,
1799                                         HTML.Tag addTag) {
1800             insertAtBoundry(editor, doc, offset, insertElement, html,
1801                             parentTag, addTag);
1802         }
1803 
1804         /**
1805          * This is invoked when inserting at a boundary. It determines
1806          * the number of pops, and then the number of pushes that need
1807          * to be performed, and then invokes insertHTML.
1808          * @deprecated As of Java 2 platform v1.3, use insertAtBoundary
1809          *
1810          * @param editor an instance of JEditorPane
1811          * @param doc an instance of HTMLDocument
1812          * @param offset an offset to start from
1813          * @param insertElement an instance of Element
1814          * @param html an HTML string
1815          * @param parentTag a parent tag
1816          * @param addTag the first tag to start inserting into document
1817          */
1818         @Deprecated
1819         protected void insertAtBoundry(JEditorPane editor, HTMLDocument doc,
1820                                        int offset, Element insertElement,
1821                                        String html, HTML.Tag parentTag,
1822                                        HTML.Tag addTag) {
1823             // Find the common parent.
1824             Element e;
1825             Element commonParent;
1826             boolean isFirst = (offset == 0);
1827 
1828             if (offset > 0 || insertElement == null) {
1829                 e = doc.getDefaultRootElement();
1830                 while (e != null && e.getStartOffset() != offset &&
1831                        !e.isLeaf()) {
1832                     e = e.getElement(e.getElementIndex(offset));
1833                 }
1834                 commonParent = (e != null) ? e.getParentElement() : null;
1835             }
1836             else {
1837                 // If inserting at the origin, the common parent is the
1838                 // insertElement.
1839                 commonParent = insertElement;
1840             }
1841             if (commonParent != null) {
1842                 // Determine how many pops to do.
1843                 int pops = 0;
1844                 int pushes = 0;
1845                 if (isFirst && insertElement != null) {
1846                     e = commonParent;
1847                     while (e != null && !e.isLeaf()) {
1848                         e = e.getElement(e.getElementIndex(offset));
1849                         pops++;
1850                     }
1851                 }
1852                 else {
1853                     e = commonParent;
1854                     offset--;
1855                     while (e != null && !e.isLeaf()) {
1856                         e = e.getElement(e.getElementIndex(offset));
1857                         pops++;
1858                     }
1859 
1860                     // And how many pushes
1861                     e = commonParent;
1862                     offset++;
1863                     while (e != null && e != insertElement) {
1864                         e = e.getElement(e.getElementIndex(offset));
1865                         pushes++;
1866                     }
1867                 }
1868                 pops = Math.max(0, pops - 1);
1869 
1870                 // And insert!
1871                 insertHTML(editor, doc, offset, html, pops, pushes, addTag);
1872             }
1873         }
1874 
1875         /**
1876          * If there is an Element with name <code>tag</code> at
1877          * <code>offset</code>, this will invoke either insertAtBoundary
1878          * or <code>insertHTML</code>. This returns true if there is
1879          * a match, and one of the inserts is invoked.
1880          */
1881         /*protected*/
1882         boolean insertIntoTag(JEditorPane editor, HTMLDocument doc,
1883                               int offset, HTML.Tag tag, HTML.Tag addTag) {
1884             Element e = findElementMatchingTag(doc, offset, tag);
1885             if (e != null && e.getStartOffset() == offset) {
1886                 insertAtBoundary(editor, doc, offset, e, html,
1887                                  tag, addTag);
1888                 return true;
1889             }
1890             else if (offset > 0) {
1891                 int depth = elementCountToTag(doc, offset - 1, tag);
1892                 if (depth != -1) {
1893                     insertHTML(editor, doc, offset, html, depth, 0, addTag);
1894                     return true;
1895                 }
1896             }
1897             return false;
1898         }
1899 
1900         /**
1901          * Called after an insertion to adjust the selection.
1902          */
1903         /* protected */
1904         void adjustSelection(JEditorPane pane, HTMLDocument doc,
1905                              int startOffset, int oldLength) {
1906             int newLength = doc.getLength();
1907             if (newLength != oldLength && startOffset < newLength) {
1908                 if (startOffset > 0) {
1909                     String text;
1910                     try {
1911                         text = doc.getText(startOffset - 1, 1);
1912                     } catch (BadLocationException ble) {
1913                         text = null;
1914                     }
1915                     if (text != null && text.length() > 0 &&
1916                         text.charAt(0) == '\n') {
1917                         pane.select(startOffset, startOffset);
1918                     }
1919                     else {
1920                         pane.select(startOffset + 1, startOffset + 1);
1921                     }
1922                 }
1923                 else {
1924                     pane.select(1, 1);
1925                 }
1926             }
1927         }
1928 
1929         /**
1930          * Inserts the HTML into the document.
1931          *
1932          * @param ae the event
1933          */
1934         public void actionPerformed(ActionEvent ae) {
1935             JEditorPane editor = getEditor(ae);
1936             if (editor != null) {
1937                 HTMLDocument doc = getHTMLDocument(editor);
1938                 int offset = editor.getSelectionStart();
1939                 int length = doc.getLength();
1940                 boolean inserted;
1941                 // Try first choice
1942                 if (!insertIntoTag(editor, doc, offset, parentTag, addTag) &&
1943                     alternateParentTag != null) {
1944                     // Then alternate.
1945                     inserted = insertIntoTag(editor, doc, offset,
1946                                              alternateParentTag,
1947                                              alternateAddTag);
1948                 }
1949                 else {
1950                     inserted = true;
1951                 }
1952                 if (adjustSelection && inserted) {
1953                     adjustSelection(editor, doc, offset, length);
1954                 }
1955             }
1956         }
1957 
1958         /** HTML to insert. */
1959         protected String html;
1960         /** Tag to check for in the document. */
1961         protected HTML.Tag parentTag;
1962         /** Tag in HTML to start adding tags from. */
1963         protected HTML.Tag addTag;
1964         /** Alternate Tag to check for in the document if parentTag is
1965          * not found. */
1966         protected HTML.Tag alternateParentTag;
1967         /** Alternate tag in HTML to start adding tags from if parentTag
1968          * is not found and alternateParentTag is found. */
1969         protected HTML.Tag alternateAddTag;
1970         /** True indicates the selection should be adjusted after an insert. */
1971         boolean adjustSelection;
1972     }
1973 
1974 
1975     /**
1976      * InsertHRAction is special, at actionPerformed time it will determine
1977      * the parent HTML.Tag based on the paragraph element at the selection
1978      * start.
1979      */
1980     @SuppressWarnings("serial") // Superclass is not serializable across versions
1981     static class InsertHRAction extends InsertHTMLTextAction {
1982         InsertHRAction() {
1983             super("InsertHR", "<hr>", null, HTML.Tag.IMPLIED, null, null,
1984                   false);
1985         }
1986 
1987         /**
1988          * Inserts the HTML into the document.
1989          *
1990          * @param ae the event
1991          */
1992         public void actionPerformed(ActionEvent ae) {
1993             JEditorPane editor = getEditor(ae);
1994             if (editor != null) {
1995                 HTMLDocument doc = getHTMLDocument(editor);
1996                 int offset = editor.getSelectionStart();
1997                 Element paragraph = doc.getParagraphElement(offset);
1998                 if (paragraph.getParentElement() != null) {
1999                     parentTag = (HTML.Tag)paragraph.getParentElement().
2000                                   getAttributes().getAttribute
2001                                   (StyleConstants.NameAttribute);
2002                     super.actionPerformed(ae);
2003                 }
2004             }
2005         }
2006 
2007     }
2008 
2009     /*
2010      * Returns the object in an AttributeSet matching a key
2011      */
2012     private static Object getAttrValue(AttributeSet attr, HTML.Attribute key) {
2013         Enumeration<?> names = attr.getAttributeNames();
2014         while (names.hasMoreElements()) {
2015             Object nextKey = names.nextElement();
2016             Object nextVal = attr.getAttribute(nextKey);
2017             if (nextVal instanceof AttributeSet) {
2018                 Object value = getAttrValue((AttributeSet)nextVal, key);
2019                 if (value != null) {
2020                     return value;
2021                 }
2022             } else if (nextKey == key) {
2023                 return nextVal;
2024             }
2025         }
2026         return null;
2027     }
2028 
2029     /*
2030      * Action to move the focus on the next or previous hypertext link
2031      * or object. TODO: This method relies on support from the
2032      * javax.accessibility package.  The text package should support
2033      * keyboard navigation of text elements directly.
2034      */
2035     @SuppressWarnings("serial") // Superclass is not serializable across versions
2036     static class NavigateLinkAction extends TextAction implements CaretListener {
2037 
2038         private static final FocusHighlightPainter focusPainter =
2039             new FocusHighlightPainter(null);
2040         private final boolean focusBack;
2041 
2042         /*
2043          * Create this action with the appropriate identifier.
2044          */
2045         public NavigateLinkAction(String actionName) {
2046             super(actionName);
2047             focusBack = "previous-link-action".equals(actionName);
2048         }
2049 
2050         /**
2051          * Called when the caret position is updated.
2052          *
2053          * @param e the caret event
2054          */
2055         public void caretUpdate(CaretEvent e) {
2056             Object src = e.getSource();
2057             if (src instanceof JTextComponent) {
2058                 JTextComponent comp = (JTextComponent) src;
2059                 HTMLEditorKit kit = getHTMLEditorKit(comp);
2060                 if (kit != null && kit.foundLink) {
2061                     kit.foundLink = false;
2062                     // TODO: The AccessibleContext for the editor should register
2063                     // as a listener for CaretEvents and forward the events to
2064                     // assistive technologies listening for such events.
2065                     comp.getAccessibleContext().firePropertyChange(
2066                         AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET,
2067                         Integer.valueOf(kit.prevHypertextOffset),
2068                         Integer.valueOf(e.getDot()));
2069                 }
2070             }
2071         }
2072 
2073         /*
2074          * The operation to perform when this action is triggered.
2075          */
2076         public void actionPerformed(ActionEvent e) {
2077             JTextComponent comp = getTextComponent(e);
2078             if (comp == null || comp.isEditable()) {
2079                 return;
2080             }
2081 
2082             Document doc = comp.getDocument();
2083             HTMLEditorKit kit = getHTMLEditorKit(comp);
2084             if (doc == null || kit == null) {
2085                 return;
2086             }
2087 
2088             // TODO: Should start successive iterations from the
2089             // current caret position.
2090             ElementIterator ei = new ElementIterator(doc);
2091             int currentOffset = comp.getCaretPosition();
2092             int prevStartOffset = -1;
2093             int prevEndOffset = -1;
2094 
2095             // highlight the next link or object after the current caret position
2096             Element nextElement;
2097             while ((nextElement = ei.next()) != null) {
2098                 String name = nextElement.getName();
2099                 AttributeSet attr = nextElement.getAttributes();
2100 
2101                 Object href = getAttrValue(attr, HTML.Attribute.HREF);
2102                 if (!(name.equals(HTML.Tag.OBJECT.toString())) && href == null) {
2103                     continue;
2104                 }
2105 
2106                 int elementOffset = nextElement.getStartOffset();
2107                 if (focusBack) {
2108                     if (elementOffset >= currentOffset &&
2109                         prevStartOffset >= 0) {
2110 
2111                         kit.foundLink = true;
2112                         comp.setCaretPosition(prevStartOffset);
2113                         moveCaretPosition(comp, kit, prevStartOffset,
2114                                           prevEndOffset);
2115                         kit.prevHypertextOffset = prevStartOffset;
2116                         return;
2117                     }
2118                 } else { // focus forward
2119                     if (elementOffset > currentOffset) {
2120 
2121                         kit.foundLink = true;
2122                         comp.setCaretPosition(elementOffset);
2123                         moveCaretPosition(comp, kit, elementOffset,
2124                                           nextElement.getEndOffset());
2125                         kit.prevHypertextOffset = elementOffset;
2126                         return;
2127                     }
2128                 }
2129                 prevStartOffset = nextElement.getStartOffset();
2130                 prevEndOffset = nextElement.getEndOffset();
2131             }
2132             if (focusBack && prevStartOffset >= 0) {
2133                 kit.foundLink = true;
2134                 comp.setCaretPosition(prevStartOffset);
2135                 moveCaretPosition(comp, kit, prevStartOffset, prevEndOffset);
2136                 kit.prevHypertextOffset = prevStartOffset;
2137             }
2138         }
2139 
2140         /*
2141          * Moves the caret from mark to dot
2142          */
2143         private void moveCaretPosition(JTextComponent comp, HTMLEditorKit kit,
2144                                        int mark, int dot) {
2145             Highlighter h = comp.getHighlighter();
2146             if (h != null) {
2147                 int p0 = Math.min(dot, mark);
2148                 int p1 = Math.max(dot, mark);
2149                 try {
2150                     if (kit.linkNavigationTag != null) {
2151                         h.changeHighlight(kit.linkNavigationTag, p0, p1);
2152                     } else {
2153                         kit.linkNavigationTag =
2154                                 h.addHighlight(p0, p1, focusPainter);
2155                     }
2156                 } catch (BadLocationException e) {
2157                 }
2158             }
2159         }
2160 
2161         private HTMLEditorKit getHTMLEditorKit(JTextComponent comp) {
2162             if (comp instanceof JEditorPane) {
2163                 EditorKit kit = ((JEditorPane) comp).getEditorKit();
2164                 if (kit instanceof HTMLEditorKit) {
2165                     return (HTMLEditorKit) kit;
2166                 }
2167             }
2168             return null;
2169         }
2170 
2171         /**
2172          * A highlight painter that draws a one-pixel border around
2173          * the highlighted area.
2174          */
2175         static class FocusHighlightPainter extends
2176             DefaultHighlighter.DefaultHighlightPainter {
2177 
2178             FocusHighlightPainter(Color color) {
2179                 super(color);
2180             }
2181 
2182             /**
2183              * Paints a portion of a highlight.
2184              *
2185              * @param g the graphics context
2186              * @param offs0 the starting model offset &ge; 0
2187              * @param offs1 the ending model offset &ge; offs1
2188              * @param bounds the bounding box of the view, which is not
2189              *               necessarily the region to paint.
2190              * @param c the editor
2191              * @param view View painting for
2192              * @return region in which drawing occurred
2193              */
2194             public Shape paintLayer(Graphics g, int offs0, int offs1,
2195                                     Shape bounds, JTextComponent c, View view) {
2196 
2197                 Color color = getColor();
2198 
2199                 if (color == null) {
2200                     g.setColor(c.getSelectionColor());
2201                 }
2202                 else {
2203                     g.setColor(color);
2204                 }
2205                 if (offs0 == view.getStartOffset() &&
2206                     offs1 == view.getEndOffset()) {
2207                     // Contained in view, can just use bounds.
2208                     Rectangle alloc;
2209                     if (bounds instanceof Rectangle) {
2210                         alloc = (Rectangle)bounds;
2211                     }
2212                     else {
2213                         alloc = bounds.getBounds();
2214                     }
2215                     g.drawRect(alloc.x, alloc.y, alloc.width - 1, alloc.height);
2216                     return alloc;
2217                 }
2218                 else {
2219                     // Should only render part of View.
2220                     try {
2221                         // --- determine locations ---
2222                         Shape shape = view.modelToView(offs0, Position.Bias.Forward,
2223                                                        offs1,Position.Bias.Backward,
2224                                                        bounds);
2225                         Rectangle r = (shape instanceof Rectangle) ?
2226                             (Rectangle)shape : shape.getBounds();
2227                         g.drawRect(r.x, r.y, r.width - 1, r.height);
2228                         return r;
2229                     } catch (BadLocationException e) {
2230                         // can't render
2231                     }
2232                 }
2233                 // Only if exception
2234                 return null;
2235             }
2236         }
2237     }
2238 
2239     /*
2240      * Action to activate the hypertext link that has focus.
2241      * TODO: This method relies on support from the
2242      * javax.accessibility package.  The text package should support
2243      * keyboard navigation of text elements directly.
2244      */
2245     @SuppressWarnings("serial") // Superclass is not serializable across versions
2246     static class ActivateLinkAction extends TextAction {
2247 
2248         /**
2249          * Create this action with the appropriate identifier.
2250          */
2251         public ActivateLinkAction(String actionName) {
2252             super(actionName);
2253         }
2254 
2255         /*
2256          * activates the hyperlink at offset
2257          */
2258         private void activateLink(String href, HTMLDocument doc,
2259                                   JEditorPane editor, int offset) {
2260             try {
2261                 URL page =
2262                     (URL)doc.getProperty(Document.StreamDescriptionProperty);
2263                 URL url = new URL(page, href);
2264                 HyperlinkEvent linkEvent = new HyperlinkEvent
2265                     (editor, HyperlinkEvent.EventType.
2266                      ACTIVATED, url, url.toExternalForm(),
2267                      doc.getCharacterElement(offset));
2268                 editor.fireHyperlinkUpdate(linkEvent);
2269             } catch (MalformedURLException m) {
2270             }
2271         }
2272 
2273         /*
2274          * Invokes default action on the object in an element
2275          */
2276         private void doObjectAction(JEditorPane editor, Element elem) {
2277             View view = getView(editor, elem);
2278             if (view != null && view instanceof ObjectView) {
2279                 Component comp = ((ObjectView)view).getComponent();
2280                 if (comp != null && comp instanceof Accessible) {
2281                     AccessibleContext ac = comp.getAccessibleContext();
2282                     if (ac != null) {
2283                         AccessibleAction aa = ac.getAccessibleAction();
2284                         if (aa != null) {
2285                             aa.doAccessibleAction(0);
2286                         }
2287                     }
2288                 }
2289             }
2290         }
2291 
2292         /*
2293          * Returns the root view for a document
2294          */
2295         private View getRootView(JEditorPane editor) {
2296             return editor.getUI().getRootView(editor);
2297         }
2298 
2299         /*
2300          * Returns a view associated with an element
2301          */
2302         private View getView(JEditorPane editor, Element elem) {
2303             Object lock = lock(editor);
2304             try {
2305                 View rootView = getRootView(editor);
2306                 int start = elem.getStartOffset();
2307                 if (rootView != null) {
2308                     return getView(rootView, elem, start);
2309                 }
2310                 return null;
2311             } finally {
2312                 unlock(lock);
2313             }
2314         }
2315 
2316         private View getView(View parent, Element elem, int start) {
2317             if (parent.getElement() == elem) {
2318                 return parent;
2319             }
2320             int index = parent.getViewIndex(start, Position.Bias.Forward);
2321 
2322             if (index != -1 && index < parent.getViewCount()) {
2323                 return getView(parent.getView(index), elem, start);
2324             }
2325             return null;
2326         }
2327 
2328         /*
2329          * If possible acquires a lock on the Document.  If a lock has been
2330          * obtained a key will be retured that should be passed to
2331          * <code>unlock</code>.
2332          */
2333         private Object lock(JEditorPane editor) {
2334             Document document = editor.getDocument();
2335 
2336             if (document instanceof AbstractDocument) {
2337                 ((AbstractDocument)document).readLock();
2338                 return document;
2339             }
2340             return null;
2341         }
2342 
2343         /*
2344          * Releases a lock previously obtained via <code>lock</code>.
2345          */
2346         private void unlock(Object key) {
2347             if (key != null) {
2348                 ((AbstractDocument)key).readUnlock();
2349             }
2350         }
2351 
2352         /*
2353          * The operation to perform when this action is triggered.
2354          */
2355         public void actionPerformed(ActionEvent e) {
2356 
2357             JTextComponent c = getTextComponent(e);
2358             if (c.isEditable() || !(c instanceof JEditorPane)) {
2359                 return;
2360             }
2361             JEditorPane editor = (JEditorPane)c;
2362 
2363             Document d = editor.getDocument();
2364             if (d == null || !(d instanceof HTMLDocument)) {
2365                 return;
2366             }
2367             HTMLDocument doc = (HTMLDocument)d;
2368 
2369             ElementIterator ei = new ElementIterator(doc);
2370             int currentOffset = editor.getCaretPosition();
2371 
2372             // invoke the next link or object action
2373             String urlString = null;
2374             String objString = null;
2375             Element currentElement;
2376             while ((currentElement = ei.next()) != null) {
2377                 String name = currentElement.getName();
2378                 AttributeSet attr = currentElement.getAttributes();
2379 
2380                 Object href = getAttrValue(attr, HTML.Attribute.HREF);
2381                 if (href != null) {
2382                     if (currentOffset >= currentElement.getStartOffset() &&
2383                         currentOffset <= currentElement.getEndOffset()) {
2384 
2385                         activateLink((String)href, doc, editor, currentOffset);
2386                         return;
2387                     }
2388                 } else if (name.equals(HTML.Tag.OBJECT.toString())) {
2389                     Object obj = getAttrValue(attr, HTML.Attribute.CLASSID);
2390                     if (obj != null) {
2391                         if (currentOffset >= currentElement.getStartOffset() &&
2392                             currentOffset <= currentElement.getEndOffset()) {
2393 
2394                             doObjectAction(editor, currentElement);
2395                             return;
2396                         }
2397                     }
2398                 }
2399             }
2400         }
2401     }
2402 
2403     private static int getBodyElementStart(JTextComponent comp) {
2404         Element rootElement = comp.getDocument().getRootElements()[0];
2405         for (int i = 0; i < rootElement.getElementCount(); i++) {
2406             Element currElement = rootElement.getElement(i);
2407             if("body".equals(currElement.getName())) {
2408                 return currElement.getStartOffset();
2409             }
2410         }
2411         return 0;
2412     }
2413 
2414     /*
2415      * Move the caret to the beginning of the document.
2416      * @see DefaultEditorKit#beginAction
2417      * @see HTMLEditorKit#getActions
2418      */
2419     @SuppressWarnings("serial") // Superclass is not serializable across versions
2420     static class BeginAction extends TextAction {
2421 
2422         /* Create this object with the appropriate identifier. */
2423         BeginAction(String nm, boolean select) {
2424             super(nm);
2425             this.select = select;
2426         }
2427 
2428         /** The operation to perform when this action is triggered. */
2429         public void actionPerformed(ActionEvent e) {
2430             JTextComponent target = getTextComponent(e);
2431             int bodyStart = getBodyElementStart(target);
2432 
2433             if (target != null) {
2434                 if (select) {
2435                     target.moveCaretPosition(bodyStart);
2436                 } else {
2437                     target.setCaretPosition(bodyStart);
2438                 }
2439             }
2440         }
2441 
2442         private boolean select;
2443     }
2444 }