Name: jk109818 Date: 04/26/2002
FULL PRODUCT VERSION :
java version "1.4.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0-b92)
Java HotSpot(TM) Client VM (build 1.4.0-b92, mixed mode)
FULL OPERATING SYSTEM VERSION : n/a
ADDITIONAL OPERATING SYSTEMS : n/a
A DESCRIPTION OF THE PROBLEM :
Getting the accurate preferred size of JList in a
JScrollPane is not possible because the combination of
behaviour in both the BasicListUI class and the
AbstractListModel class.
For instance, if you create a JList in a JScrollPane (no
horiz scrollbar) with a single String "Foo", you would
expect the horizontal size of the list to be enough to
exactly accommodate the String "Foo". If you now add the
string "FooBar" to the model, you would expect the
preferred size to immediately report a size that
accommodates "FooBar". This is not the case.
This behaviour is because the preferred size of the JList
is delegated to BasicListIU in the look and feel packages.
The method BasicListUI.getPreferredSize() caches certain
values like cell width when working out the preferred size
of the JList. This cache is only marked stale on a
ListDataEvent.
In other words, the size of list is only fully recomputed
when the list data changes. While the assumption is
correct, the look and feel has no 'special' API for
receiving these events and instead must add itself as an
ordinary listener to the ListModel just like any other
swing component. To make matters worse, the
AbstractListModel iterates through its listeners in reverse
order of insertion thereby guarenteeing that the
BasicListUI is the last listener to receive the event
(unless the user switches Look and Feel midway through the
application - then the UI will get the event first!).
The net effect is that it is impossible to determine a
JList's new preferred size on any available event hook.
This makes things like NarrowingComboBoxes (as seen in
IntelliJ's IDEA) impossible to create with some serious
hacking.
See the code example for a demonstration of the bug.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Run the application
2. Check the checkbox.
3. The list data changes, but the JList's size won't (until
you resize the frame).
4. Console output shows the difference between the reported
preferred sizes when using an the event hook, and just
after the event has taken place (the deferred output)
EXPECTED VERSUS ACTUAL BEHAVIOR :
Expected:
FlowLayout honours the preferred size of a component, so I
expect the scrollpane to resize when I check the checkbox.
I also expect the getPreferredSize method to reflect the
size needed for the new layout.
Actual:
The scrollpane does not resize and the reported
getPreferred size is wrong (it is using invalid cached
data).
The Swing worker thread is used to show that the preferred
size is actually being properly recalculated, just too late
to be of any use.
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
public class TestScrollPaneBug extends JPanel {
public static void main(String[] args) {
JPanel panel = new JPanel(new BorderLayout());
final JList list = new JList();
final JScrollPane scrollPane = new JScrollPane(list);
scrollPane.setHorizontalScrollBarPolicy
(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
final DefaultListModel listModel = new DefaultListModel();
list.setModel(listModel);
listModel.add(0, "Short");
listModel.addListDataListener(new ListDataListener() {
public void intervalAdded(ListDataEvent e) {
update();
}
public void intervalRemoved(ListDataEvent e) {
update();
}
public void contentsChanged(ListDataEvent e) {
update();
}
private void update() {
System.out.println("scrollPane preferred size = " +
scrollPane.getPreferredSize());
SwingUtilities.invokeLater(new Runnable() {
public void run() {
System.out.println("scrollPane preferred size
(deferred) = " + scrollPane.getPreferredSize());
}
});
}
});
// To preserve the preferred width contract
JPanel flowLayoutPanel = new JPanel(new FlowLayout());
flowLayoutPanel.add(scrollPane);
panel.add(flowLayoutPanel, BorderLayout.CENTER);
JCheckBox checkBox = new JCheckBox("Long description");
checkBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
listModel.set(0, "Very very very long description");
} else {
listModel.set(0, "Short");
}
list.setModel(listModel);
}
});
panel.add(checkBox, BorderLayout.SOUTH);
JFrame frame = new JFrame();
frame.setContentPane(panel);
frame.pack();
frame.show();
}
}
---------- END SOURCE ----------
CUSTOMER WORKAROUND :
There are two hacks that may work and I've only tested one.
The first hack is to use the SwingUtilities.invokeLater()
service to run any code that may need a valid preferred
size. This does seems to work while simulataneously
seeming like a horrific abuse of Threading.
The second hack is to get the collection of
listDataListeners from the ListModel; find the UI delegate
and send it the event yourself before requesting the
preferred size. While this avoids the threading issues, it
does require more inter-class knowledge which may have been
avoided otherwise.
(Review ID: 144875)
======================================================================