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