First: never-ever tweak the sizing hints on the component level. Especially not so when you do have a powerful LayoutManager such as MigLayout which supports tweaking on the manager level.
In code, adjusting the pref size of whatever:
// calculate some width to add to pref, f.i. to take the scrollbar width into account
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
// **do not**
comp.setPreferredSize(new Dimension(comp.getPreferredSize().width + prefBarWidth, ...);
// **do**
String pref = "(pref+" + prefBarWidth + "px)";
content.add(pane, "width " + pref);
That said: basically, you hit a (arguable) bug in ScrollPaneLayout. While it looks like taking the scrollbar width into account, it actually doesn't in all cases. The relevant snippet from preferredLayoutSize
// filling the sizes used for calculating the pref
Dimension extentSize = null;
Dimension viewSize = null;
Component view = null;
if (viewport != null) {
extentSize = viewport.getPreferredSize();
view = viewport.getView();
if (view != null) {
viewSize = view.getPreferredSize();
} else {
viewSize = new Dimension(0, 0);
}
}
....
// the part trying to take the scrollbar width into account
if ((vsb != null) && (vsbPolicy != VERTICAL_SCROLLBAR_NEVER)) {
if (vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS) {
prefWidth += vsb.getPreferredSize().width;
}
else if ((viewSize != null) && (extentSize != null)) {
boolean canScroll = true;
if (view instanceof Scrollable) {
canScroll = !((Scrollable)view).getScrollableTracksViewportHeight();
}
if (canScroll &&
// following condition is the **culprit**
(viewSize.height > extentSize.height)) {
prefWidth += vsb.getPreferredSize().width;
}
}
}
it's the culprit, because
- it's comparing the view pref against the viewport pref
- they are the same most of the time
The result is what you are seeing: the scrollbar overlaps (in the sense of cutting off some width) the view.
A hack around is a custom ScrollPaneLayout which adds the scrollbar width if the view's height is less than the actual viewport height, a crude example (beware: not production quality) to play with
public static class MyScrollPaneLayout extends ScrollPaneLayout {
@Override
public Dimension preferredLayoutSize(Container parent) {
Dimension dim = super.preferredLayoutSize(parent);
JScrollPane pane = (JScrollPane) parent;
Component comp = pane.getViewport().getView();
Dimension viewPref = comp.getPreferredSize();
Dimension port = pane.getViewport().getExtentSize();
// **Edit 2** changed condition to <= to prevent jumping
if (port.height < viewPref.height) {
dim.width += pane.getVerticalScrollBar().getPreferredSize().width;
}
return dim;
}
}
Edit
hmm ... see the jumping (between showing vs. not showing the vertical scrollbar, as described in the comment): when I replace the textfield in my example with another scrollPane, then resizing "near" its pref width exhibits the problem. So the hack isn't good enough, could be that the time of asking the viewport for its extent is incorrect (in the middle of the layout process). Currently no idea, how to do better.
Edit 2
tentative tracking: when doing a pixel-by-pixel width change, it feels like a one-off error. Changing the condition from <
to <=
seems to fix the jumping - at the price of always adding the the scrollbar width. So on the whole, this leads to step one with a broader trailing inset ;-) Meanwhile believing that the whole logic of the scollLlayout needs to be improved ...
To summarize your options:
- adjust the pref width in a (MigLayout) componentConstraint. It's the simplest, drawback is an addional trailing white space in case the scrollbar is not showing
- fix the scrollPaneLayout. Requires some effort and tests (see the code of core ScrollPaneLayout what needs to be done), the advantage is a consistent padding w/out the scrollbar
- not an option manually set the pref width on the component
Below are code examples to play with:
// adjust the pref width in the component constraint
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
comp.add(new JLabel("some item: "));
comp.add(new JTextField(i + 5));
}
MigLayout outer = new MigLayout("wrap 2",
"[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
String pref = "(pref+" + prefBarWidth + "px)";
content.add(pane, "width " + pref);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {
@Override
public void actionPerformed(ActionEvent e) {
int count = (comp.getComponentCount() +1)/ 2;
comp.add(new JLabel("some Item: "));
comp.add(new JTextField(count + 5));
pane.getParent().revalidate();
}
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);
// use a custom ScrollPaneLayout
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
comp.add(new JLabel("some item: "));
comp.add(new JTextField(i + 5));
}
MigLayout outer = new MigLayout("wrap 2",
"[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
pane.setLayout(new MyScrollPaneLayout());
content.add(pane);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {
@Override
public void actionPerformed(ActionEvent e) {
int count = (comp.getComponentCount() +1)/ 2;
comp.add(new JLabel("some Item: "));
comp.add(new JTextField(count + 5));
pane.getParent().revalidate();
}
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);