001/* JEditorPane.java --
002   Copyright (C) 2002, 2004, 2005, 2006,  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing;
040
041import java.awt.Container;
042import java.awt.Dimension;
043import java.io.BufferedInputStream;
044import java.io.FilterInputStream;
045import java.io.IOException;
046import java.io.InputStream;
047import java.io.InputStreamReader;
048import java.io.Reader;
049import java.io.StringReader;
050import java.net.MalformedURLException;
051import java.net.URL;
052import java.net.URLConnection;
053import java.util.HashMap;
054
055import javax.accessibility.AccessibleContext;
056import javax.accessibility.AccessibleHyperlink;
057import javax.accessibility.AccessibleHypertext;
058import javax.accessibility.AccessibleStateSet;
059import javax.accessibility.AccessibleText;
060import javax.swing.event.HyperlinkEvent;
061import javax.swing.event.HyperlinkListener;
062import javax.swing.plaf.TextUI;
063import javax.swing.text.AbstractDocument;
064import javax.swing.text.BadLocationException;
065import javax.swing.text.DefaultEditorKit;
066import javax.swing.text.Document;
067import javax.swing.text.EditorKit;
068import javax.swing.text.Element;
069import javax.swing.text.JTextComponent;
070import javax.swing.text.View;
071import javax.swing.text.ViewFactory;
072import javax.swing.text.WrappedPlainView;
073import javax.swing.text.html.HTML;
074import javax.swing.text.html.HTMLDocument;
075import javax.swing.text.html.HTMLEditorKit;
076
077/**
078 * A powerful text editor component that can handle different types of
079 * content.
080 *
081 * The JEditorPane text component is driven by an instance of
082 * {@link EditorKit}. The editor kit is responsible for providing
083 * a default {@link Document} implementation, a mechanism for loading
084 * and saving documents of its supported content type and providing
085 * a set of {@link Action}s for manipulating the content.
086 *
087 * By default the following content types are supported:
088 * <ul>
089 * <li><code>text/plain</code>: Plain text, handled by
090 *   {@link javax.swing.text.DefaultEditorKit}.</li>
091 * <li><code>text/html</code>: HTML 4.0 styled text, handled by
092 *   {@link javax.swing.text.html.HTMLEditorKit}.</li>
093 * <li><code>text/rtf</code>: RTF text, handled by
094 *   {@link javax.swing.text.rtf.RTFEditorKit}.</li>
095 * </ul>
096 *
097 * @author original author unknown
098 * @author Roman Kennke (roman@kennke.org)
099 * @author Anthony Balkissoon abalkiss at redhat dot com
100 */
101public class JEditorPane extends JTextComponent
102{
103  /**
104   * Provides accessibility support for <code>JEditorPane</code>.
105   *
106   * @author Roman Kennke (kennke@aicas.com)
107   */
108  protected class AccessibleJEditorPane extends AccessibleJTextComponent
109  {
110
111    /**
112     * Creates a new <code>AccessibleJEditorPane</code> object.
113     */
114    protected AccessibleJEditorPane()
115    {
116      super();
117    }
118
119    /**
120     * Returns a description of this <code>AccessibleJEditorPane</code>. If
121     * this property is not set, then this returns the content-type of the
122     * editor pane.
123     *
124     * @return a description of this AccessibleJEditorPane
125     */
126    public String getAccessibleDescription()
127    {
128      String descr = super.getAccessibleDescription();
129      if (descr == null)
130        return getContentType();
131      else
132        return descr;
133    }
134
135    /**
136     * Returns the accessible state of this <code>AccessibleJEditorPane</code>.
137     *
138     * @return  the accessible state of this <code>AccessibleJEditorPane</code>
139     */
140    public AccessibleStateSet getAccessibleStateSet()
141    {
142      AccessibleStateSet state = super.getAccessibleStateSet();
143      // TODO: Figure out what state must be added here to the super's state.
144      return state;
145    }
146  }
147
148  /**
149   * Provides accessibility support for <code>JEditorPane</code>s, when the
150   * editor kit is an instance of {@link HTMLEditorKit}.
151   *
152   * @author Roman Kennke (kennke@aicas.com)
153   */
154  protected class AccessibleJEditorPaneHTML extends AccessibleJEditorPane
155  {
156    /**
157     * Returns the accessible text of the <code>JEditorPane</code>. This will
158     * be an instance of
159     * {@link JEditorPaneAccessibleHypertextSupport}.
160     *
161     * @return the accessible text of the <code>JEditorPane</code>
162     */
163    public AccessibleText getAccessibleText()
164    {
165      return new JEditorPaneAccessibleHypertextSupport();
166    }
167  }
168
169  /**
170   * This is the accessible text that is returned by
171   * {@link AccessibleJEditorPaneHTML#getAccessibleText()}.
172   *
173   * @author Roman Kennke (kennke@aicas.com)
174   */
175  protected class JEditorPaneAccessibleHypertextSupport
176    extends AccessibleJEditorPane implements AccessibleHypertext
177  {
178
179    /**
180     * Creates a new JEditorPaneAccessibleHypertextSupport object.
181     */
182    public JEditorPaneAccessibleHypertextSupport()
183    {
184      super();
185    }
186
187    /**
188     * The accessible representation of a HTML link.
189     *
190     * @author Roman Kennke (kennke@aicas.com)
191     */
192    public class HTMLLink extends AccessibleHyperlink
193    {
194
195      /**
196       * The element in the document that represents the link.
197       */
198      Element element;
199
200      /**
201       * Creates a new <code>HTMLLink</code>.
202       *
203       * @param el the link element
204       */
205      public HTMLLink(Element el)
206      {
207        this.element = el;
208      }
209
210      /**
211       * Returns <code>true</code> if this <code>HTMLLink</code> is still
212       * valid. A <code>HTMLLink</code> can become invalid when the document
213       * changes.
214       *
215       * @return <code>true</code> if this <code>HTMLLink</code> is still
216       *         valid
217       */
218      public boolean isValid()
219      {
220        // I test here if the element at our element's start offset is the
221        // same as the element in the document at this offset. If this is true,
222        // I consider the link valid, if not, then this link no longer
223        // represented by this HTMLLink and therefor invalid.
224        HTMLDocument doc = (HTMLDocument) getDocument();
225        return doc.getCharacterElement(element.getStartOffset()) == element;
226      }
227
228      /**
229       * Returns the number of AccessibleActions in this link object. In
230       * general, link have 1 AccessibleAction associated with them. There are
231       * special cases where links can have multiple actions associated, like
232       * in image maps.
233       *
234       * @return the number of AccessibleActions in this link object
235       */
236      public int getAccessibleActionCount()
237      {
238        // TODO: Implement the special cases.
239        return 1;
240      }
241
242      /**
243       * Performs the specified action on the link object. This ususally means
244       * activating the link.
245       *
246       * @return <code>true</code> if the action has been performed
247       *         successfully, <code>false</code> otherwise
248       */
249      public boolean doAccessibleAction(int i)
250      {
251        String href = (String) element.getAttributes().getAttribute("href");
252        HTMLDocument doc = (HTMLDocument) getDocument();
253        try
254          {
255            URL url = new URL(doc.getBase(), href);
256            setPage(url);
257            String desc = doc.getText(element.getStartOffset(),
258                            element.getEndOffset() - element.getStartOffset());
259            HyperlinkEvent ev =
260              new HyperlinkEvent(JEditorPane.this,
261                                 HyperlinkEvent.EventType.ACTIVATED, url, desc,
262                                 element);
263            fireHyperlinkUpdate(ev);
264            return true;
265          }
266        catch (Exception ex)
267          {
268            return false;
269          }
270      }
271
272      /**
273       * Returns the description of the action at action index <code>i</code>.
274       * This method returns the text within the element associated with this
275       * link.
276       *
277       * @param i the action index
278       *
279       * @return the description of the action at action index <code>i</code>
280       */
281      public String getAccessibleActionDescription(int i)
282      {
283        HTMLDocument doc = (HTMLDocument) getDocument();
284        try
285          {
286            return doc.getText(element.getStartOffset(),
287                            element.getEndOffset() - element.getStartOffset());
288          }
289        catch (BadLocationException ex)
290          {
291            throw (AssertionError)
292            new AssertionError("BadLocationException must not be thrown "
293                               + "here.")
294              .initCause(ex);
295          }
296      }
297
298      /**
299       * Returns an {@link URL} object, that represents the action at action
300       * index <code>i</code>.
301       *
302       * @param i the action index
303       *
304       * @return an {@link URL} object, that represents the action at action
305       *         index <code>i</code>
306       */
307      public Object getAccessibleActionObject(int i)
308      {
309        String href = (String) element.getAttributes().getAttribute("href");
310        HTMLDocument doc = (HTMLDocument) getDocument();
311        try
312          {
313            URL url = new URL(doc.getBase(), href);
314            return url;
315          }
316        catch (MalformedURLException ex)
317          {
318            return null;
319          }
320      }
321
322      /**
323       * Returns an object that represents the link anchor. For examples, if
324       * the link encloses a string, then a <code>String</code> object is
325       * returned, if the link encloses an &lt;img&gt; tag, then an
326       * <code>ImageIcon</code> object is returned.
327       *
328       * @return an object that represents the link anchor
329       */
330      public Object getAccessibleActionAnchor(int i)
331      {
332        // TODO: This is only the String case. Implement all cases.
333        return getAccessibleActionDescription(i);
334      }
335
336      /**
337       * Returns the start index of the hyperlink element.
338       *
339       * @return the start index of the hyperlink element
340       */
341      public int getStartIndex()
342      {
343        return element.getStartOffset();
344      }
345
346      /**
347       * Returns the end index of the hyperlink element.
348       *
349       * @return the end index of the hyperlink element
350       */
351      public int getEndIndex()
352      {
353        return element.getEndOffset();
354      }
355
356    }
357
358    /**
359     * Returns the number of hyperlinks in the document.
360     *
361     * @return the number of hyperlinks in the document
362     */
363    public int getLinkCount()
364    {
365      HTMLDocument doc = (HTMLDocument) getDocument();
366      HTMLDocument.Iterator linkIter = doc.getIterator(HTML.Tag.A);
367      int count = 0;
368      while (linkIter.isValid())
369        {
370          count++;
371          linkIter.next();
372        }
373      return count;
374    }
375
376    /**
377     * Returns the <code>i</code>-th hyperlink in the document or
378     * <code>null</code> if there is no hyperlink with the specified index.
379     *
380     * @param i the index of the hyperlink to return
381     *
382     * @return the <code>i</code>-th hyperlink in the document or
383     *         <code>null</code> if there is no hyperlink with the specified
384     *         index
385     */
386    public AccessibleHyperlink getLink(int i)
387    {
388      HTMLDocument doc = (HTMLDocument) getDocument();
389      HTMLDocument.Iterator linkIter = doc.getIterator(HTML.Tag.A);
390      int count = 0;
391      while (linkIter.isValid())
392        {
393          count++;
394          if (count == i)
395            break;
396          linkIter.next();
397        }
398      if (linkIter.isValid())
399        {
400          int offset = linkIter.getStartOffset();
401          // TODO: I fetch the element for the link via getCharacterElement().
402          // I am not sure that this is correct, maybe we must use
403          // getParagraphElement()?
404          Element el = doc.getCharacterElement(offset);
405          HTMLLink link = new HTMLLink(el);
406          return link;
407        }
408      else
409        return null;
410    }
411
412    /**
413     * Returns the index of the link element at the character position
414     * <code>c</code> within the document, or <code>-1</code> if there is no
415     * link at the specified position.
416     *
417     * @param c the character index from which to fetch the link index
418     *
419     * @return the index of the link element at the character position
420     *         <code>c</code> within the document, or <code>-1</code> if there
421     *         is no link at the specified position
422     */
423    public int getLinkIndex(int c)
424    {
425      HTMLDocument doc = (HTMLDocument) getDocument();
426      HTMLDocument.Iterator linkIter = doc.getIterator(HTML.Tag.A);
427      int count = 0;
428      while (linkIter.isValid())
429        {
430          if (linkIter.getStartOffset() <= c && linkIter.getEndOffset() > c)
431            break;
432          count++;
433          linkIter.next();
434        }
435      if (linkIter.isValid())
436        return count;
437      else
438        return -1;
439    }
440
441    /**
442     * Returns the link text of the link at index <code>i</code>, or
443     * <code>null</code>, if there is no link at the specified position.
444     *
445     * @param i the index of the link
446     *
447     * @return  the link text of the link at index <code>i</code>, or
448     *          <code>null</code>, if there is no link at the specified
449     *          position
450     */
451    public String getLinkText(int i)
452    {
453      HTMLDocument doc = (HTMLDocument) getDocument();
454      HTMLDocument.Iterator linkIter = doc.getIterator(HTML.Tag.A);
455      int count = 0;
456      while (linkIter.isValid())
457        {
458          count++;
459          if (count == i)
460            break;
461          linkIter.next();
462        }
463      if (linkIter.isValid())
464        {
465          int offset = linkIter.getStartOffset();
466          // TODO: I fetch the element for the link via getCharacterElement().
467          // I am not sure that this is correct, maybe we must use
468          // getParagraphElement()?
469          Element el = doc.getCharacterElement(offset);
470          try
471            {
472              String text = doc.getText(el.getStartOffset(),
473                                      el.getEndOffset() - el.getStartOffset());
474              return text;
475            }
476          catch (BadLocationException ex)
477            {
478              throw (AssertionError)
479                new AssertionError("BadLocationException must not be thrown "
480                                   + "here.")
481                  .initCause(ex);
482            }
483        }
484      else
485        return null;
486    }
487  }
488
489  /**
490   * Used to store a mapping for content-type to editor kit class.
491   */
492  private static class EditorKitMapping
493  {
494    /**
495     * The classname of the editor kit.
496     */
497    String className;
498
499    /**
500     * The classloader with which the kit is to be loaded.
501     */
502    ClassLoader classLoader;
503
504    /**
505     * Creates a new EditorKitMapping object.
506     *
507     * @param cn the classname
508     * @param cl the classloader
509     */
510    EditorKitMapping(String cn, ClassLoader cl)
511    {
512      className = cn;
513      classLoader = cl;
514    }
515  }
516
517  /**
518   * An EditorKit used for plain text. This is the default editor kit for
519   * JEditorPanes.
520   *
521   * @author Roman Kennke (kennke@aicas.com)
522   */
523  private static class PlainEditorKit extends DefaultEditorKit
524  {
525
526    /**
527     * Returns a ViewFactory that supplies WrappedPlainViews.
528     */
529    public ViewFactory getViewFactory()
530    {
531      return new ViewFactory()
532      {
533        public View create(Element el)
534        {
535          return new WrappedPlainView(el);
536        }
537      };
538    }
539  }
540
541  /**
542   * A special stream that can be cancelled.
543   */
544  private class PageStream
545    extends FilterInputStream
546  {
547    /**
548     * True when the stream has been cancelled, false otherwise.
549     */
550    private boolean cancelled;
551
552    protected PageStream(InputStream in)
553    {
554      super(in);
555      cancelled = false;
556    }
557
558    private void checkCancelled()
559      throws IOException
560    {
561      if (cancelled)
562        throw new IOException("Stream has been cancelled");
563    }
564
565    void cancel()
566    {
567      cancelled = true;
568    }
569
570    public int read()
571      throws IOException
572    {
573      checkCancelled();
574      return super.read();
575    }
576
577    public int read(byte[] b, int off, int len)
578      throws IOException
579    {
580      checkCancelled();
581      return super.read(b, off, len);
582    }
583
584    public long skip(long n)
585      throws IOException
586    {
587      checkCancelled();
588      return super.skip(n);
589    }
590
591    public int available()
592      throws IOException
593    {
594      checkCancelled();
595      return super.available();
596    }
597
598    public void reset()
599      throws IOException
600    {
601      checkCancelled();
602      super.reset();
603    }
604  }
605
606  /**
607   * The thread that loads documents asynchronously.
608   */
609  private class PageLoader
610    implements Runnable
611  {
612    private Document doc;
613    private PageStream in;
614    private URL old;
615    URL page;
616    PageLoader(Document doc, InputStream in, URL old, URL page)
617    {
618      this.doc = doc;
619      this.in = new PageStream(in);
620      this.old = old;
621      this.page = page;
622    }
623
624    public void run()
625    {
626      try
627        {
628          read(in, doc);
629        }
630      catch (IOException ex)
631        {
632          UIManager.getLookAndFeel().provideErrorFeedback(JEditorPane.this);
633        }
634      finally
635        {
636          if (SwingUtilities.isEventDispatchThread())
637            firePropertyChange("page", old, page);
638          else
639            {
640              SwingUtilities.invokeLater(new Runnable()
641              {
642                public void run()
643                {
644                  firePropertyChange("page", old, page);
645                }
646              });
647            }
648         }
649     }
650
651     void cancel()
652     {
653       in.cancel();
654     }
655  }
656
657  private static final long serialVersionUID = 3140472492599046285L;
658
659  private EditorKit editorKit;
660
661  boolean focus_root;
662
663  /**
664   * Maps content-types to editor kit instances.
665   */
666  static HashMap editorKits;
667
668  // A mapping between content types and registered EditorKit types
669  static HashMap registerMap;
670
671  static
672  {
673    registerMap = new HashMap();
674    editorKits = new HashMap();
675    registerEditorKitForContentType("application/rtf",
676                                    "javax.swing.text.rtf.RTFEditorKit");
677    registerEditorKitForContentType("text/plain",
678                                    "javax.swing.JEditorPane$PlainEditorKit");
679    registerEditorKitForContentType("text/html",
680                                    "javax.swing.text.html.HTMLEditorKit");
681    registerEditorKitForContentType("text/rtf",
682                                    "javax.swing.text.rtf.RTFEditorKit");
683
684  }
685
686  // A mapping between content types and used EditorKits
687  HashMap editorMap;
688
689  /**
690   * The currently loading stream, if any.
691   */
692  private PageLoader loader;
693
694  public JEditorPane()
695  {
696    init();
697    setEditorKit(createDefaultEditorKit());
698  }
699
700  public JEditorPane(String url) throws IOException
701  {
702    this(new URL(url));
703  }
704
705  public JEditorPane(String type, String text)
706  {
707    init();
708    setEditorKit(createEditorKitForContentType(type));
709    setText(text);
710  }
711
712  public JEditorPane(URL url) throws IOException
713  {
714    init();
715    setEditorKit(createEditorKitForContentType("text/html"));
716    setPage(url);
717  }
718
719  /**
720   * Called by the constructors to set up the default bindings for content
721   * types and EditorKits.
722   */
723  void init()
724  {
725    editorMap = new HashMap();
726  }
727
728  protected EditorKit createDefaultEditorKit()
729  {
730    return new PlainEditorKit();
731  }
732
733  /**
734   * Creates and returns an EditorKit that is appropriate for the given
735   * content type.  This is created using the default recognized types
736   * plus any EditorKit types that have been registered.
737   *
738   * @see #registerEditorKitForContentType(String, String)
739   * @see #registerEditorKitForContentType(String, String, ClassLoader)
740   * @param type the content type
741   * @return an EditorKit for use with the given content type
742   */
743  public static EditorKit createEditorKitForContentType(String type)
744  {
745    // Try cached instance.
746    EditorKit e = (EditorKit) editorKits.get(type);
747    if (e == null)
748      {
749        EditorKitMapping m = (EditorKitMapping) registerMap.get(type);
750        if (m != null)
751          {
752            String className = m.className;
753            ClassLoader loader = m.classLoader;
754            try
755              {
756                e = (EditorKit) loader.loadClass(className).newInstance();
757              }
758            catch (Exception e2)
759              {
760                // The reference implementation returns null when class is not
761                // loadable or instantiatable.
762              }
763          }
764        // Cache this for later retrieval.
765        if (e != null)
766          editorKits.put(type, e);
767      }
768    return e;
769  }
770
771  /**
772   * Sends a given <code>HyperlinkEvent</code> to all registered listeners.
773   *
774   * @param event the event to send
775   */
776  public void fireHyperlinkUpdate(HyperlinkEvent event)
777  {
778    HyperlinkListener[] listeners = getHyperlinkListeners();
779
780    for (int index = 0; index < listeners.length; ++index)
781       listeners[index].hyperlinkUpdate(event);
782  }
783
784  /**
785   * Returns the accessible context associated with this editor pane.
786   *
787   * @return the accessible context associated with this editor pane
788   */
789  public AccessibleContext getAccessibleContext()
790  {
791    if (accessibleContext == null)
792      {
793        if (getEditorKit() instanceof HTMLEditorKit)
794          accessibleContext = new AccessibleJEditorPaneHTML();
795        else
796          accessibleContext = new AccessibleJEditorPane();
797      }
798    return accessibleContext;
799  }
800
801  public final String getContentType()
802  {
803    return getEditorKit().getContentType();
804  }
805
806  /**
807   * Returns the EditorKit. If there is no EditorKit set this method
808   * calls createDefaultEditorKit() and setEditorKit() first.
809   */
810  public EditorKit getEditorKit()
811  {
812    if (editorKit == null)
813      setEditorKit(createDefaultEditorKit());
814    return editorKit;
815  }
816
817  /**
818   * Returns the class name of the EditorKit associated with the given
819   * content type.
820   *
821   * @since 1.3
822   * @param type the content type
823   * @return the class name of the EditorKit associated with this content type
824   */
825  public static String getEditorKitClassNameForContentType(String type)
826  {
827    EditorKitMapping m = (EditorKitMapping) registerMap.get(type);
828    String kitName = m != null ? m.className : null;
829    return kitName;
830  }
831
832  /**
833   * Returns the EditorKit to use for the given content type.  If an
834   * EditorKit has been explicitly set via
835   * <code>setEditorKitForContentType</code>
836   * then it will be returned.  Otherwise an attempt will be made to create
837   * an EditorKit from the default recognzied content types or any
838   * EditorKits that have been registered.  If none can be created, a
839   * PlainEditorKit is created.
840   *
841   * @see #registerEditorKitForContentType(String, String)
842   * @see #registerEditorKitForContentType(String, String, ClassLoader)
843   * @param type the content type
844   * @return an appropriate EditorKit for the given content type
845   */
846  public EditorKit getEditorKitForContentType(String type)
847  {
848    // First check if an EditorKit has been explicitly set.
849    EditorKit e = (EditorKit) editorMap.get(type);
850    // Then check to see if we can create one.
851    if (e == null)
852      {
853        e = createEditorKitForContentType(type);
854        if (e != null)
855          setEditorKitForContentType(type, e);
856      }
857    // Otherwise default to PlainEditorKit.
858    if (e == null)
859      e = createDefaultEditorKit();
860    return e;
861  }
862
863  /**
864   * Returns the preferred size for the JEditorPane. This is implemented to
865   * return the super's preferred size, unless one of
866   * {@link #getScrollableTracksViewportHeight()} or
867   * {@link #getScrollableTracksViewportWidth()} returns <code>true</code>,
868   * in which case the preferred width and/or height is replaced by the UI's
869   * minimum size.
870   *
871   * @return the preferred size for the JEditorPane
872   */
873  public Dimension getPreferredSize()
874  {
875    Dimension pref = super.getPreferredSize();
876    Container parent = getParent();
877    if (parent instanceof JViewport)
878      {
879        JViewport vp = (JViewport) getParent();
880        TextUI ui = getUI();
881        Dimension min = null;
882        if (! getScrollableTracksViewportWidth())
883          {
884            min = ui.getMinimumSize(this);
885            int vpWidth = vp.getWidth();
886            if (vpWidth != 0 && vpWidth < min.width)
887              pref.width = min.width;
888          }
889        if (! getScrollableTracksViewportHeight())
890          {
891            if (min == null)
892              min = ui.getMinimumSize(this);
893            int vpHeight = vp.getHeight();
894            if (vpHeight != 0 && vpHeight < min.height)
895              pref.height = min.height;
896          }
897      }
898    return pref;
899  }
900
901  /**
902   * Returns <code>true</code> when a Viewport should force the height of
903   * this component to match the viewport height. This is implemented to return
904   * <code>true</code> when  the parent is an instance of JViewport and
905   * the viewport height > the UI's minimum height.
906   *
907   * @return <code>true</code> when a Viewport should force the height of
908   *         this component to match the viewport height
909   */
910  public boolean getScrollableTracksViewportHeight()
911  {
912    // Tests show that this returns true when the parent is a JViewport
913    // and has a height > minimum UI height.
914    Container parent = getParent();
915    int height = parent.getHeight();
916    TextUI ui = getUI();
917    return parent instanceof JViewport
918           && height >= ui.getMinimumSize(this).height
919           && height <= ui.getMaximumSize(this).height;
920  }
921
922  /**
923   * Returns <code>true</code> when a Viewport should force the width of
924   * this component to match the viewport width. This is implemented to return
925   * <code>true</code> when  the parent is an instance of JViewport and
926   * the viewport width > the UI's minimum width.
927   *
928   * @return <code>true</code> when a Viewport should force the width of
929   *         this component to match the viewport width
930   */
931  public boolean getScrollableTracksViewportWidth()
932  {
933    // Tests show that this returns true when the parent is a JViewport
934    // and has a width > minimum UI width.
935    Container parent = getParent();
936    return parent != null && parent instanceof JViewport
937           && parent.getWidth() > getUI().getMinimumSize(this).width;
938  }
939
940  public URL getPage()
941  {
942    return loader != null ? loader.page : null;
943  }
944
945  protected InputStream getStream(URL page)
946    throws IOException
947  {
948    URLConnection conn = page.openConnection();
949    // Try to detect the content type of the stream data.
950    String type = conn.getContentType();
951    if (type != null)
952      setContentType(type);
953    InputStream stream = conn.getInputStream();
954    return new BufferedInputStream(stream);
955  }
956
957  public String getText()
958  {
959    return super.getText();
960  }
961
962  public String getUIClassID()
963  {
964    return "EditorPaneUI";
965  }
966
967  public boolean isFocusCycleRoot()
968  {
969    return focus_root;
970  }
971
972  protected String paramString()
973  {
974    return "JEditorPane";
975  }
976
977  /**
978   * This method initializes from a stream.
979   */
980  public void read(InputStream in, Object desc) throws IOException
981  {
982    EditorKit kit = getEditorKit();
983    if (kit instanceof HTMLEditorKit && desc instanceof HTMLDocument)
984      {
985        HTMLDocument doc = (HTMLDocument) desc;
986        setDocument(doc);
987        try
988          {
989            InputStreamReader reader = new InputStreamReader(in);
990            kit.read(reader, doc, 0);
991          }
992        catch (BadLocationException ex)
993          {
994            assert false : "BadLocationException must not be thrown here.";
995          }
996      }
997    else
998      {
999        Reader inRead = new InputStreamReader(in);
1000        super.read(inRead, desc);
1001      }
1002  }
1003
1004  /**
1005   * Establishes a binding between type and classname.  This enables
1006   * us to create an EditorKit later for the given content type.
1007   *
1008   * @param type the content type
1009   * @param classname the name of the class that is associated with this
1010   * content type
1011   */
1012  public static void registerEditorKitForContentType(String type,
1013                                                     String classname)
1014  {
1015    registerEditorKitForContentType(type, classname,
1016                               Thread.currentThread().getContextClassLoader());
1017  }
1018
1019  /**
1020   * Establishes the default bindings of type to classname.
1021   */
1022  public static void registerEditorKitForContentType(String type,
1023                                                     String classname,
1024                                                     ClassLoader loader)
1025  {
1026    registerMap.put(type, new EditorKitMapping(classname, loader));
1027  }
1028
1029  /**
1030   * Replaces the currently selected content with new content represented
1031   * by the given string.
1032   */
1033  public void replaceSelection(String content)
1034  {
1035    // TODO: Implement this properly.
1036    super.replaceSelection(content);
1037  }
1038
1039  /**
1040   * Scrolls the view to the given reference location (that is, the value
1041   * returned by the UL.getRef method for the URL being displayed).
1042   */
1043  public void scrollToReference(String reference)
1044  {
1045    // TODO: Implement this properly.
1046  }
1047
1048  public final void setContentType(String type)
1049  {
1050    // Strip off content type parameters.
1051    int paramIndex = type.indexOf(';');
1052    if (paramIndex > -1)
1053      {
1054        // TODO: Handle character encoding.
1055        type = type.substring(0, paramIndex).trim();
1056      }
1057    if (editorKit != null
1058        && editorKit.getContentType().equals(type))
1059      return;
1060
1061    EditorKit kit = getEditorKitForContentType(type);
1062
1063    if (kit != null)
1064      setEditorKit(kit);
1065  }
1066
1067  public void setEditorKit(EditorKit newValue)
1068  {
1069    if (editorKit == newValue)
1070      return;
1071
1072    if (editorKit != null)
1073      editorKit.deinstall(this);
1074
1075    EditorKit oldValue = editorKit;
1076    editorKit = newValue;
1077
1078    if (editorKit != null)
1079      {
1080        editorKit.install(this);
1081        setDocument(editorKit.createDefaultDocument());
1082      }
1083
1084    firePropertyChange("editorKit", oldValue, newValue);
1085    invalidate();
1086    repaint();
1087    // Reset the accessibleContext since this depends on the editorKit.
1088    accessibleContext = null;
1089  }
1090
1091  /**
1092   * Explicitly sets an EditorKit to be used for the given content type.
1093   * @param type the content type
1094   * @param k the EditorKit to use for the given content type
1095   */
1096  public void setEditorKitForContentType(String type, EditorKit k)
1097  {
1098    editorMap.put(type, k);
1099  }
1100
1101  /**
1102   * Sets the current URL being displayed.
1103   */
1104  public void setPage(String url) throws IOException
1105  {
1106    setPage(new URL(url));
1107  }
1108
1109  /**
1110   * Sets the current URL being displayed.
1111   */
1112  public void setPage(URL page) throws IOException
1113  {
1114    if (page == null)
1115      throw new IOException("invalid url");
1116
1117    URL old = getPage();
1118    // Only reload if the URL doesn't point to the same file.
1119    // This is not the same as equals because there might be different
1120    // URLs on the same file with different anchors.
1121    if (old == null || ! old.sameFile(page))
1122      {
1123        InputStream in = getStream(page);
1124        if (editorKit != null)
1125          {
1126            Document doc = editorKit.createDefaultDocument();
1127            doc.putProperty(Document.StreamDescriptionProperty, page);
1128
1129            if (loader != null)
1130              loader.cancel();
1131            loader = new PageLoader(doc, in, old, page);
1132
1133            int prio = -1;
1134            if (doc instanceof AbstractDocument)
1135              {
1136                AbstractDocument aDoc = (AbstractDocument) doc;
1137                prio = aDoc.getAsynchronousLoadPriority();
1138              }
1139            if (prio >= 0)
1140              {
1141                // Load asynchronously.
1142                setDocument(doc);
1143                Thread loadThread = new Thread(loader,
1144                                               "JEditorPane.PageLoader");
1145                loadThread.setDaemon(true);
1146                loadThread.setPriority(prio);
1147                loadThread.start();
1148              }
1149            else
1150              {
1151                // Load synchronously.
1152                loader.run();
1153                setDocument(doc);
1154              }
1155          }
1156      }
1157  }
1158
1159  /**
1160   * Sets the text of the JEditorPane.  The argument <code>t</code>
1161   * is expected to be in the format of the current EditorKit.  This removes
1162   * the content of the current document and uses the EditorKit to read in the
1163   * new text.  This allows the EditorKit to handle the String rather than just
1164   * inserting in plain text.
1165   *
1166   * @param t the text to display in this JEditorPane
1167   */
1168  public void setText(String t)
1169  {
1170    try
1171    {
1172      // Remove the current content.
1173      Document doc = getDocument();
1174      doc.remove(0, doc.getLength());
1175      if (t == null || t.equals(""))
1176        return;
1177
1178      // Let the EditorKit read the text into the Document.
1179      getEditorKit().read(new StringReader(t), doc, 0);
1180    }
1181    catch (BadLocationException ble)
1182    {
1183      // TODO: Don't know what to do here.
1184    }
1185    catch (IOException ioe)
1186    {
1187      // TODO: Don't know what to do here.
1188    }
1189  }
1190
1191  /**
1192   * Add a <code>HyperlinkListener</code> object to this editor pane.
1193   *
1194   * @param listener the listener to add
1195   */
1196  public void addHyperlinkListener(HyperlinkListener listener)
1197  {
1198    listenerList.add(HyperlinkListener.class, listener);
1199  }
1200
1201  /**
1202   * Removes a <code>HyperlinkListener</code> object to this editor pane.
1203   *
1204   * @param listener the listener to remove
1205   */
1206  public void removeHyperlinkListener(HyperlinkListener listener)
1207  {
1208    listenerList.remove(HyperlinkListener.class, listener);
1209  }
1210
1211  /**
1212   * Returns all added <code>HyperlinkListener</code> objects.
1213   *
1214   * @return array of listeners
1215   *
1216   * @since 1.4
1217   */
1218  public HyperlinkListener[] getHyperlinkListeners()
1219  {
1220    return (HyperlinkListener[]) getListeners(HyperlinkListener.class);
1221  }
1222}