Search code examples
javahtmlswinganchorjeditorpane

JEditorPane.scrollToReference() with dynamically-generated HTML


I can't figure out how JEditorPane.scrollToReference() is meant to be used with a dynamically-generated HTML page. I'd like to open a dialog box with a JEditorPane that is scrolled to an anchor of my choosing. No matter how I approach the problem, the view is always scrolled at the bottom of the page.

As an ugly workaround, I currently parse the entire HTML document for anchor tags, save their offsets in a Map<String, Integer>, and then I call:

editorPane.setCaretPosition(anchorMap.get("anchor-name"));

...which doesn't even produce an attractive result, because the caret position within the visible rectangle is seemingly unpredictable, and is rarely at the top of the window. I'm looking for a more browser-like behavior where the anchored text appears at the top of the visible area.

Below is a screenshot of what happens with my awkward workaround (there is an anchor on "header 6"), without having touched the scroll bar:

screenshot

I think I'm missing something but I can't figure out what exactly.

Source of my current workaround:

import javax.swing.*;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.*;
import javax.swing.text.html.HTMLEditorKit.*;
import javax.swing.text.html.parser.*;
import java.io.*;
import java.awt.*;
import java.util.HashMap;

public class AnchorTest
{
    public static void main(String[] args)
    {
        final String html = generateLongPage();
        final HashMap<String, Integer> anchors = anchorPositions(html);

        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                JEditorPane editor  = new JEditorPane("text/html", html);
                JScrollPane scroller= new JScrollPane(editor);

                scroller.setPreferredSize(new Dimension(400, 250));

                //editor.scrollToReference("anchor6");  // doesn't work...
                editor.setCaretPosition(anchors.get("anchor6")); //sorta works

                JOptionPane.showMessageDialog(new JPanel(), scroller, "",
                        JOptionPane.PLAIN_MESSAGE);
            }});
    }

    public static HashMap<String, Integer> anchorPositions(String html)
    {
        final HashMap<String, Integer> map = new HashMap<String, Integer>();

        Reader reader = new StringReader(html);
        HTMLEditorKit.Parser parser = new ParserDelegator();

        try
        {
            ParserCallback cb = new ParserCallback()
            {
                public void handleStartTag(HTML.Tag t,
                                           MutableAttributeSet a,
                                           int pos)
                {
                    if (t == HTML.Tag.A) {
                        String name = 
                            (String)a.getAttribute(HTML.Attribute.NAME);
                        map.put(name, pos);
                    }
                }
            };

            parser.parse(reader, cb, true);
        }
        catch (IOException ignore) {}

        return map;
    }


    public static String generateLongPage()
    {
        StringBuilder sb = new StringBuilder(
                "<html><head><title>hello</title></head><body>\n");

        for (int i = 0; i < 10; i++) {
            sb.append(String.format(
                    "<h1><a name='anchor%d'>header %d</a></h1>\n<p>", i, i));

            for (int j = 0; j < 100; j++) {
                sb.append("blah ");
            }
            sb.append("</p>\n\n");
        }

        return sb.append("</body></html>").toString();
    }
}

Solution

  • The problem is probably because the pane is not visible yet or not laid out yet when scrollToReference is executed, so its size cannot be determined.

    The implementation of scrollToReference has the following block:

    Rectangle r = modelToView(iter.getStartOffset());
    if (r != null) {
       ...
       scrollRectToVisible(r);
    }
    

    In the posted example the rectangle is null for anchor6, since the view is not yet visible or its size is not positive.

    Try this dirty fix to delay scrolling:

    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            editor.scrollToReference("anchor6");
        }
    });
    JOptionPane.showMessageDialog(new JPanel(), scroller, "",
            JOptionPane.PLAIN_MESSAGE);