001    /* JEditorPane.java --
002       Copyright (C) 2002, 2004, 2005, 2006,  Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing;
040    
041    import java.awt.Container;
042    import java.awt.Dimension;
043    import java.io.BufferedInputStream;
044    import java.io.FilterInputStream;
045    import java.io.IOException;
046    import java.io.InputStream;
047    import java.io.InputStreamReader;
048    import java.io.Reader;
049    import java.io.StringReader;
050    import java.net.MalformedURLException;
051    import java.net.URL;
052    import java.net.URLConnection;
053    import java.util.HashMap;
054    
055    import javax.accessibility.AccessibleContext;
056    import javax.accessibility.AccessibleHyperlink;
057    import javax.accessibility.AccessibleHypertext;
058    import javax.accessibility.AccessibleStateSet;
059    import javax.accessibility.AccessibleText;
060    import javax.swing.event.HyperlinkEvent;
061    import javax.swing.event.HyperlinkListener;
062    import javax.swing.plaf.TextUI;
063    import javax.swing.text.AbstractDocument;
064    import javax.swing.text.BadLocationException;
065    import javax.swing.text.DefaultEditorKit;
066    import javax.swing.text.Document;
067    import javax.swing.text.EditorKit;
068    import javax.swing.text.Element;
069    import javax.swing.text.JTextComponent;
070    import javax.swing.text.View;
071    import javax.swing.text.ViewFactory;
072    import javax.swing.text.WrappedPlainView;
073    import javax.swing.text.html.HTML;
074    import javax.swing.text.html.HTMLDocument;
075    import 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     */
101    public 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    }