I'd like to decorate a JPanel with a JLayer, but am failing to understand why doing so messes up this panel being laid out inside a JScrollPane. The decorated component is supposed to act as a drop in replacement, but it does not appear to work in this case.
The following code creates two equivalent JPanels and puts them into another panel with CardLayout (so you may switch between them using the buttons). The only difference is that in one case the panel is decorated with a JLayer.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.plaf.LayerUI;
public class JLayerScroll extends JFrame {
public JLayerScroll() {
setTitle("Jumpy border");
setLayout(new BorderLayout());
setDefaultCloseOperation(EXIT_ON_CLOSE);
createGui();
setSize(400, 200);
setLocationRelativeTo(null);
}
private void createGui() {
JPanel mainPanel = new JPanel(new CardLayout());
// two panels ("label panels"), first one decorated, second one not
mainPanel.add(createSingleScrolledComponent(new JLayer<JPanel>(createLabelPanel(), new LayerUI<JPanel>())), WITH_JLAYER);
mainPanel.add(createSingleScrolledComponent(createLabelPanel()), WITHOUT_JLAYER);
add(mainPanel);
createButtons(mainPanel);
}
private JPanel createSingleScrolledComponent(Component component) {
GridBagConstraints gbc;
JScrollPane scroll;
JPanel panel = new JPanel(new GridBagLayout());
scroll = new JScrollPane(component);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0d;
gbc.weighty = 1.0d;
panel.add(scroll, gbc);
return panel;
}
private JPanel createLabelPanel() {
GridBagConstraints gbc;
JPanel panel = new JPanel(new GridBagLayout());
JPanel entry = new JPanel(new GridBagLayout());
entry.setBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, Color.red));
JLabel label = new JLabel("Some input:");
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
entry.add(label, gbc);
JTextField field = new JTextField(20);
field.setMinimumSize(new Dimension(field.getPreferredSize().width, field.getMinimumSize().height));
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = 0;
entry.add(field, gbc);
gbc = new GridBagConstraints();
gbc.gridx = 2;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0d;
entry.add(Box.createHorizontalGlue(), gbc);
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0d;
panel.add(entry, gbc);
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 1;
gbc.fill = GridBagConstraints.VERTICAL;
gbc.weighty = 1.0d;
panel.add(Box.createVerticalGlue(), gbc);
return panel;
}
private static final String WITH_JLAYER = "with-jlayer";
private static final String WITHOUT_JLAYER = "no-jlayer";
private void createButtons(final JPanel mainPanel) {
GridBagConstraints gbc;
JButton button;
JPanel actionsPanel = new JPanel(new GridBagLayout());
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0d;
actionsPanel.add(Box.createHorizontalGlue(), gbc);
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = 0;
button = new JButton("With JLayer");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
CardLayout layout = (CardLayout) mainPanel.getLayout();
layout.show(mainPanel, WITH_JLAYER);
}
});
actionsPanel.add(button, gbc);
gbc.gridx = 2;
gbc.gridy = 0;
button = new JButton("Without JLayer");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
CardLayout layout = (CardLayout) mainPanel.getLayout();
layout.show(mainPanel, WITHOUT_JLAYER);
}
});
actionsPanel.add(button, gbc);
add(actionsPanel, BorderLayout.SOUTH);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new JLayerScroll().setVisible(true);
}
});
}
}
If you pay attention to the red border in the two cases, you will notice that in one case it spans the whole available horizontal space (as expected), while in the other case, it only spans around visible components (label and text field).
Why is this happening and how do I achieve the same behavior as in the undecorated case (spanning through all available horizontal space)?
The only difference is that in one case the panel is decorated with a JLayer.
A difference between a JPanel
and a JLayer
is that the JLayer
implements the Scrollable
interface but a JPanel
does not.
The getScrollableTracksViewportWidth()
method controls whether the component added to the viewport should be displayed at its preferred size or the width of the viewport. The JLayer delegates to the JPanel, but since JPanel doesn't implement the Scrollable interface the JLayer implementation will return "false" which means the component should be displayed at its preferred width.
So a way to get around this is to use a wrapper panel for the JLayer
:
//scroll = new JScrollPane(component);
JPanel wrapper = new JPanel( new BorderLayout() );
wrapper.add( component );
scroll = new JScrollPane(wrapper);
Now the component added to the viewport of the scrollpane is a JPanel in both cases, so they should behave the same way.