(Note: After writing this, I found a solution, which appears in the addendum of this post.)
To reproduce the problem, you need to make the scroll bars required. (This is why some people have trouble reproducing this bug.) This means the obvious workaround is to make your horizontal scroll bar optional. (This is not always practical.)
You will only see the bug when you drag the window's width out to more than 1200 pixels or so. Until then, the scroll bar will work fine.
And the problem only shows up in Nimbus. (It may show up in other L&Fs created from the SynthLookAndFeel, but I haven't investigated that yet.)
I've found that the spurious scroll bar thumb only shows up when you have no need to scroll, so it's just a visual bug. When you need to scroll, the scroll bar thumb will appear and will work properly, although it might not be the right size. This may be why it hasn't been fixed yet.
Here's an example where you can compare the different L&Fs. In this example, Choose Nimbus, then drag the width inward and watch how the size of the scroll bar changes. When you're wider than the background image, the spurious scroll bar will be visible. As soon as you get narrower, a valid scroll bar thumb will appear, but it will be a bit too small. As you get smaller, the scroll bar thumb will stay a constant size until you reach a certain point, (at viewport width of 1282 pixels) then it will start getting smaller like it's supposed to.
With any other L&F, as soon as you get narrower than the background image, a thumb will appear that almost fills its space. It gets smaller as the window gets smaller, like it's supposed to.
(This exercise will also reveal how Nimbus draws much more slowly than any other L&F.)
You can observe different, but still incorrect behavior by making the icon smaller. Try 800 x 450. The spurious scroll bar will appear when the viewport width is > 1035. (Viewport size is shown at the bottom of the window.)
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* NimbusScrollBug
* <p/>
* @author Miguel Muñoz
*/
public class NimbusScrollBug extends JPanel {
private static final long serialVersionUID = -4235866781219951631L;
private static JFrame frame;
private static boolean firstTime = true;
private static Point location;
private static final UIManager.LookAndFeelInfo[] INFOS
= UIManager.getInstalledLookAndFeels();
private final JLabel viewPortLabel = new JLabel();
public static void main(final String[] args) {
makeMainFrame(new NimbusScrollBug(), "System");
}
public static void makeMainFrame(final NimbusScrollBug mainPanel,
final String name) {
if (firstTime) {
installLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
frame = new JFrame(name);
final Container contentPane = frame.getContentPane();
contentPane.setLayout(new BorderLayout());
contentPane.add(mainPanel, BorderLayout.CENTER);
contentPane.add(makeButtonPane(mainPanel), BorderLayout.LINE_START);
frame.setLocationByPlatform(firstTime);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.setVisible(true);
if (firstTime) {
location = frame.getLocation();
} else {
frame.setLocation(location);
}
frame.addComponentListener(new ComponentAdapter() {
@Override
public void componentMoved(final ComponentEvent e) {
location = e.getComponent().getLocation();
}
});
firstTime = false;
}
private static JPanel makeButtonPane(final NimbusScrollBug mainPanel) {
JPanel innerButtonPanel = new JPanel(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.BOTH;
constraints.gridx = 0; // forces vertical layout.
for (final UIManager.LookAndFeelInfo lAndF : INFOS) {
final JButton button = new JButton(lAndF.getName());
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
frame.dispose();
installLookAndFeel(lAndF.getClassName());
makeMainFrame(new NimbusScrollBug(), lAndF.getName());
}
});
innerButtonPanel.add(button, constraints);
}
final String version = System.getProperty("java.version");
JLabel versionLabel = new JLabel("Java Version " + version);
innerButtonPanel.add(versionLabel, constraints);
JPanel outerButtonPanel = new JPanel(new BorderLayout());
outerButtonPanel.add(innerButtonPanel, BorderLayout.PAGE_START);
return outerButtonPanel;
}
private static void installLookAndFeel(final String className) {
//noinspection OverlyBroadCatchBlock
try {
UIManager.setLookAndFeel(className);
} catch (Exception e) {
//noinspection ProhibitedExceptionThrown
throw new RuntimeException(e);
}
}
private NimbusScrollBug() {
Icon icon = new Icon() {
@Override
public void paintIcon(final Component c, final Graphics g,
final int x, final int y) {
Graphics2D g2 = (Graphics2D) g;
g2.translate(x, y);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Stroke lineStroke = new BasicStroke(6.0f);
g2.setStroke(lineStroke);
g2.setColor(Color.white);
g2.fillRect(0, 0, getIconWidth(), getIconHeight());
g2.setColor(Color.RED);
g2.drawLine(0, 0, getIconWidth(), getIconHeight());
g2.drawLine(0, getIconHeight(), getIconWidth(), 0);
g2.dispose();
}
@Override
public int getIconWidth() {
return 1600;
}
@Override
public int getIconHeight() {
return 900;
}
};
JLabel label = new JLabel(icon);
setLayout(new BorderLayout());
final JScrollPane scrollPane = new JScrollPane(label,
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
label.addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorResized(final HierarchyEvent e) {
viewPortLabel.setText("ViewPort Size: "
+ scrollPane.getViewport().getSize());
}
});
add(scrollPane, BorderLayout.CENTER);
add(viewPortLabel, BorderLayout.PAGE_END);
}
}
Addendum: Further investigation revealed the problem. The NimbusDefaults class, which creates the UIDefaults instance for Nimbus, has this line:
d.put("ScrollBar.maximumThumbSize", new DimensionUIResource(1000, 1000));
Any other look and feel uses 4096 for both values (so, for really big monitors, they will show the same behavior).
The following method, which may be used to install any look and feel, will fix this problem:
private static void installLookAndFeel(final String className) {
//noinspection OverlyBroadCatchBlock
try {
Class<?> lnfClass = Class.forName(className, true,
Thread.currentThread().getContextClassLoader());
final LookAndFeel lAndF;
lAndF = (LookAndFeel) lnfClass.getConstructor().newInstance();
// Reset the defaults after instantiating, but before
// calling UIManager.setLookAndFeel(). This fixes the Nimbus bug
DimensionUIResource dim = new DimensionUIResource(4096, 4096);
lAndF.getDefaults().put("ScrollBar.maximumThumbSize", dim);
UIManager.setLookAndFeel(lAndF);
} catch (Exception e) {
final String systemName = UIManager.getSystemLookAndFeelClassName();
// Prevents an infinite recursion that's not very likely...
// (I like to code defensively)
if (!className.equals(systemName)) {
installLookAndFeel(systemName);
} else {
// Feel free to handle this any other way.
//noinspection ProhibitedExceptionThrown
throw new RuntimeException(e);
}
}
}
Of course, you can fix the problem for really big monitors by using a bigger value.
I confirmed that the vertical scroll bar has exactly the same problem, but is only seen when the window gets very large vertically. This is why this problem is usually only seen with the horizontal scroll bar.