Possible race condition in JEditorPane.scrollToReference
This message will be a bit long, so I will first post a summary of the problem, and then give more details.
Summary:
I have written an application that uses a subclass of JEditorPane to display HTML. The JEditorPane is contained within a JScrollPane. When a URL with a reference (page.html#anchor) is loaded, the JEditorPane briefly scrolls to the correct location, and then (sometimes) scrolls a bit more to the wrong location. Whether the extra scrolling occurs seems to depend on the length of the page. Whenever the extra scrolling happens, the JEditorPane receives a resize event AFTER the call to scrollToReference. If it works correctly, this extra resize event does NOT occur.
Unfortunately, it doesn't seem possible to easily reproduce this problem with a small amount of code, so a longer explanation of what's happening is in order.
Details
The JEditorPane subclass uses its own EditorKit, derived from HTMLEditorKit. The main reason is to override the read method. This method first calls the parent, and then adds some content to the document, representing annotations about various portions of the document.
One possibility is that scrollToReference is being called before my custom content is being added; however, placing print statements in the code shows that this is not true. Even so, I have tried both synchronous loading of the document within the HTMLEditorKit, by changing the AsynchronousLoadPriority of the default document created by my custom EditorKit. The problem is still exhibited. Since the loading is synchronous, and is happening in the Event Dispatch Thread, it seems that the call to scrollToReference should be put at the end of the queue (JEdtiorPane uses invokeLater to make scrollToReference be called on the EDT, and the documentation for invokeLater claims that the event queue will be drained before the Runnable added via invokeLater is executed).
When I run the code in a debugger and break at the call to scrollToReference and let it execute, the JEditorPane is indeed scrolled to the correct location. Note that the scrollbars do not get adjusted at this point to indicate the new location within the document. Continuing execution causes the extra scrolling to occur.
I've tried several other things, but I think I'll leave it at that and see if anyone has any ideas on what is generating the extra scrolling. I've been banging my head against the wall on this one on and off for a couple of years, so any insights would be greatly appreciated.
I find that if you are adding custom code and you want to be certain that it executes at the end of some process, then you need to use SwingUtilities.invokeLater(...) to add your code to the end of the EDT.
========================================
I find that if you are adding custom code and you want to be certain that it executes at the end of some process, then you need to use SwingUtilities.invokeLater(...) to add your code to the end of the EDT.
========================================
In general, I would agree with this. However, in this specific case, the code that is adding content to the document is running in the event dispatch thread, when the document is being loaded synchronously.
The JEditorPane code doesn't call scrollToReference until after the reading of the document is completed (with either synchronous or asynchronous loading), and it does so using invokeLater. So even if my code was running outside of the EDT (which it does when the document is loaded asynchronously), any events my code generates should be added to the event queue -before- the call to scrollToReference, since it's added using invokeLater.
Even though I was convinced this isn't the problem by the preceding argument, I did try it, and this did not improve the situation.
Two things,
\One, if small sections of code dont reproduce the problem, chances are the problem is in the bigger section of code.
Two: If you are loading it synchronously, are you making sure that the threads are finishing at the proper times? IE: You aren't doing something silly like mocing to an anchor that is 90% through a huge document, when the document isn't entirely finished? Because there is a chance that what is happening is:
Page loaded 50%
Anchor jumps to 45% location
Page finishes loading
the 45% mark has moved so the scrollpane relocates.
OK, I have solved this problem and am posting the solution so that those of you that end up looking at this thread through a search engine aren't frustrated.
There is indeed a race condition here. But, thankfully, it is one that is preventable. The race is between the event dispatch thread building the views of the document, and the ImageView class, which by default loads images asynchronously, in a thread other than the event dispatch thread.
So, when scrollToReference is called by JEditorPane, even though the document has been completely parsed and all of the View objects have been created to display the document, the location of the referenced point is still subject to change. This is because the ImageView class has a "not loaded yet" icon which it displays while it loads the image in the background; this icon is 39 pixels in size. When the actual image is loaded, the size of the document changes, and the scroll pane moves to a different spot.
To fix this, you need to override the ViewFactory that creates ImageView objects for img tags, and tell it to load the images synchronously. Doing it this way obviously incurs some perceived performance penalties, so I have implemented it so that it only behaves this way when the URL being loaded contains a reference. Here is my custom ImageView class:
public class CustomImageView extends ImageView {
/**
* Constructs a new view of the image element <i>elem</i>
* @param elem the element representing the image
* @param contentPane the ContentPane loading the document, referenced
* to determine if the URL being loaded contained a reference
* @see ContentPane#isPageContainedReference()
*/
public CustomImageView (Element elem, ContentPane contentPane) {
super (elem);
setLoadsSynchronously(contentPane.isPageContainedReference());
}
}
In this example, the class ContentPane is a sub-class of JEditorPane.
I have yet to decide if I think this is a bug in the Java libraries; my inclination is to say yes, but I haven't spent the time to isolate where it occurs, and given that I've spent so much time on this already, I don't have the patience to do it.
It seems like a scenario similar to what Baggins posted is likely what's happening, although I think that this could be solved fairly easily by making sure that whatever is causing the relocation is intelligent enough to know when the document size has changed.
