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