Skip to main content

Scrollable revisited

5 replies [Last post]
pietblok
Offline
Joined: 2003-07-17
Points: 0

Hi Alex,

There have been a couple of threads in this forum related to Scrollable. One of the issues is the way most Swing classes that implement Scrollable query for their parent being a JViewport. You suggested that you are planning to change that in JDK 7.

I have been thinking about this and wondered if there may be another way to cope with those components that depend on their parent being a JViewport.

What if we, artificially, have a JViewport placed in between the JXLayer and its view? Of course, that JViewport should be a modified JViewport.

I very much would like to hear your opinion on this.

I did some simple tests to see what would happen and, as far I can see, it looks promising.

However, there is a problem that I can't really solve and don't quite understand, but that I can more or less work around.

The problem:

The mock JViewport must implement Scrollable itself to propagate method calls to the view. The method getScrollableTracksViewportHeight(...) , when delegated to the view, seems to get into some recursive sequence, causing tremendous flickering and consuming lots of CPU cycles.

The workaround

Only for the method getScrollableTracksViewportHeight the call is not delegated to the view.

The code

The MockViewport

<br />
import java.awt.Component;<br />
import java.awt.Dimension;<br />
import java.awt.Rectangle;</p>
<p>import javax.swing.JViewport;<br />
import javax.swing.Scrollable;<br />
import javax.swing.SwingConstants;</p>
<p>public class MockViewport extends JViewport implements Scrollable {</p>
<p>    private static final long serialVersionUID = 1L;</p>
<p>    private final boolean doHeight;</p>
<p>    public MockViewport(boolean doHeight) {<br />
	this.doHeight = doHeight;<br />
    }</p>
<p>    @Override<br />
    public Dimension getPreferredScrollableViewportSize() {<br />
	if (this.getView() instanceof Scrollable) {<br />
	    return ((Scrollable) this.getView())<br />
		    .getPreferredScrollableViewportSize();<br />
	} else {<br />
	    return this.getPreferredSize();<br />
	}<br />
    }</p>
<p>    @Override<br />
    public int getScrollableBlockIncrement(Rectangle visibleRect,<br />
	    int orientation, int direction) {<br />
	if (this.getView() instanceof Scrollable) {<br />
	    return ((Scrollable) this.getView()).getScrollableBlockIncrement(<br />
		    visibleRect, orientation, direction);<br />
	} else {<br />
	    return (orientation == SwingConstants.VERTICAL) ? visibleRect.height<br />
		    : visibleRect.width;<br />
	}<br />
    }</p>
<p>    /**<br />
     * This is problematic because it may trigger recursion. Why?<br />
     */<br />
    @Override<br />
    public boolean getScrollableTracksViewportHeight() {<br />
	Component view = this.getView();<br />
	if (doHeight) {<br />
	    if (view instanceof Scrollable) {<br />
		return ((Scrollable) view)<br />
			.getScrollableTracksViewportHeight();<br />
	    }<br />
	}<br />
	if (view != null) {<br />
	    int myHeight = this.getHeight();<br />
	    return (myHeight == 0 || view.getPreferredSize().height < myHeight);<br />
	}<br />
	return true;<br />
    }</p>
<p>    @Override<br />
    public boolean getScrollableTracksViewportWidth() {<br />
	if (this.getView() instanceof Scrollable) {<br />
	    return ((Scrollable) this.getView())<br />
		    .getScrollableTracksViewportWidth();<br />
	} else {<br />
	    return false;<br />
	}<br />
    }</p>
<p>    @Override<br />
    public int getScrollableUnitIncrement(Rectangle visibleRect,<br />
	    int orientation, int direction) {<br />
	if (this.getView() instanceof Scrollable) {<br />
	    return ((Scrollable) this.getView()).getScrollableUnitIncrement(<br />
		    visibleRect, orientation, direction);<br />
	} else {<br />
	    return 1;<br />
	}<br />
    }</p>
<p>    @Override<br />
    public void scrollRectToVisible(Rectangle rect) {<br />
	super.scrollRectToVisible(rect);<br />
    }</p>
<p>}<br />

A test

<br />
import javax.swing.JComponent;<br />
import javax.swing.JFrame;<br />
import javax.swing.JScrollPane;<br />
import javax.swing.JTextPane;<br />
import javax.swing.JViewport;<br />
import javax.swing.SwingUtilities;</p>
<p>import org.jdesktop.jxlayer.JXLayer;<br />
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;</p>
<p>public class TestMockViewport {</p>
<p>    public static void createGUI(JComponent target, String title, int index) {<br />
	JFrame frame = new JFrame(title);<br />
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);<br />
	frame.add(target);<br />
	frame.setSize(200, 300);<br />
	frame.setLocation(10 + index * 205, 100);<br />
	frame.setVisible(true);<br />
    }</p>
<p>    public static JComponent createScrollable() {<br />
	JTextPane textPane = new JTextPane();<br />
	textPane.setContentType("text/html");<br />
	StringBuilder sb = new StringBuilder();<br />
	sb.append("Test Scrollable");<br />
	for (int index = 0; index < 2; index++) {<br />
	    sb<br />
		    .append("This is a test of the propagation of the Scrollable interface.");<br />
	}<br />
	sb.append("");<br />
	textPane.setText(sb.toString());<br />
	return textPane;<br />
    }</p>
<p>    public static void main(String[] args) {<br />
	SwingUtilities.invokeLater(new Runnable() {</p>
<p>	    @Override<br />
	    public void run() {<br />
		createGUI(noJXLayer(), "No JXLayer", 0);<br />
		createGUI(simpleJXLayer(), "Simple JXLayer", 1);<br />
		createGUI(mockViewport(false), "Mock Viewport", 2);<br />
		createGUI(mockViewport(true), "Mock recursive", 3);<br />
	    }<br />
	});<br />
    }</p>
<p>    public static JComponent mockViewport(boolean doHeight) {<br />
	JViewport mock = new MockViewport(doHeight);<br />
	mock.setView(createScrollable());<br />
	return new JScrollPane(new JXLayer(mock,<br />
		new AbstractLayerUI()));<br />
    }</p>
<p>    public static JComponent noJXLayer() {<br />
	return new JScrollPane(createScrollable());<br />
    }</p>
<p>    public static JComponent simpleJXLayer() {<br />
	return new JScrollPane(new JXLayer(createScrollable(),<br />
		new AbstractLayerUI()));<br />
    }</p>
<p>}<br />

When you run the test, you will see four frames that have a different setup for a JTextPane.

1) No JXLayer used. The text pane is directly set as the view for a JScrollPane. No problem.

2) JXLayer is used with an instance of AbstractLayerUI as the LayerUI. The well known problem: the text pane thinks "No JViewport", so gives misguiding information via the Scrollable methods.

3) JXLayer is used, but the view for JXLayer is a MockViewport that in turn has the text pane as its view. No problem, everything seems fine. But what if the text pane, or any other component, had some real implemtation for getScrollableTracksViewportHeight(...) ?

4) Same as three, but call to getScrollableTracksViewportHeight(...) are delegated to the view of the mock viewport. Problem: when you resize the frame until its height is insufficient to contain the text and a vertical scrollbar is needed, tremendoud flickering starts.

Piet

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
pietblok
Offline
Joined: 2003-07-17
Points: 0

Update:

Removed the update text because it was in error.

Message was edited by: pietblok

alexfromsun
Offline
Joined: 2005-09-05
Points: 0

Hello Piet

Thanks for the code!
I need a bit more time for review...

You removed the updated code, what was the problem with that?
Does it mean that problem with recursion is still present?
Why does MockViewport implement Scrollable?

Thanks
alexp

pietblok
Offline
Joined: 2003-07-17
Points: 0

Hi Alex,

> You removed the updated code, what was the problem
> with that?
> Does it mean that problem with recursion is still
> present?

Unfortunately, yes.

> Why does MockViewport implement Scrollable?

The MockViewport will be in between the Scrollable JXLayer and a Scrollable view that expects its parent to be a JViewport. The hierarchy will be as follows:

1 JScrollPane (top level)
2 JXLayer with its LayerUI
3 MockViewport
4 The intended Scrollable view, for example a JTextPane

The JScrollPane detects that JXLayer is Scrollable and asks for info.

In turn, JXLayer with its LayerUI detects that its actual view (the MockViewport) is Scrollable and delegates for info.

The MockViewport detects that its view (the JTextPane) is Scrollable and delegates for info.

The other way around:

The JTextPane needs info from its JViewport parent (the MockViewport) to decide what info to return. When it has decided, the info will eventually be delivered to the JScrollPane.

The problem seems to have something to do with the fact that the JScrollPane repeatedly asks for info when it needs to add or remove sliders, because that changes the size of its own JViewport, which in turn invalidates the info previously gathered.

In short, it proves to be much more difficult to design such a totally neutral and transparent mediator viewport than I initially thought! It will keep me off the streets for a while I'm afraid.

Piet

pietblok
Offline
Joined: 2003-07-17
Points: 0

Update.

Meanwhile, after some sleepless nights, I think I understand why a MockViewport doesn't work and why it never will work.

The reason: the mock viewport has a builtin ambiguity regarding its own size (and derivates like preferred size). Let me explain with the following example hierarchy (I left JXLayer out, because it has nothing to do with the problem):

JScrollPane
JViewport
MockViewport
JTextPane

During negotiation for the best size for the text pane, the text pane queries the mock viewport for its size (the preferred size chosen depends on the view port size). During these queries, the mock viewport should mimic the size of its parent real view port.

But, the real viewport/scrollpane also queries the mock viewport for its size, to determine whether or not scroll bars must be applied. Now the mock viewport should mimic its child text pane's size.

I see no way for a component to differentiate between calls from its parent and calls from its child (or calls from third parties, or calls from itself).

Now I also have a better understanding of why the current implementations of Scrollable in some standard JComponent types are the way they are.

The results of this exercise are disappointing, but, again, I learned a lot.

Piet

alexfromsun
Offline
Joined: 2005-09-05
Points: 0

Hello Piet

Thank you for your outstanding analysis of this problem,
it helps me to understand Swing better

reading posts from you is always a pleasure for me

alexp