1

I'm trying to develop a form of an accordion menu. There are a small number (2-12) options that can be toggled on/off. When toggled on, there will be a JPanel with additional settings that become visible. When toggled off, the additional settings will not be visible.

I've created a SelectableExpandablePanel class that extends JPanel and implements ActionListener and ComponentListener. The panel holds two things - a JToggleButton and a child Component (which will typically be a JPanel, but I don't want to limit myself for future reuse of this concept) in a BoxLayout to enforce one column. When the toggle button is selected, the child becomes visible. When the toggle is deselected, the child is hidden.

When I use this component, I intend to put it on a JPanel inside of a JScrollPane, as demonstrated in the sample main method.

There appear to be two problems that I'm having trouble overcoming:

  1. If I don't specify a JFrame size, it's only large enough for the width of each child and tall enough for three buttons. When I click on the button, I would expect the JScrollPane to do its thing and generate a vertical scroll bar. This isn't happening.

  2. I'd like the toggle buttons to be the full width of the JPanel that contains them. I thought what I did in the constructor plus the Component Listener would handle that, but it doesn't.

What is provided below compiles and has a main method. If compiled and executed, it drives the component I'm building to provide a test frame and the ability to reproduce the issues I'm talking about.

import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;

public class SelectableExpandablePanel extends JPanel implements
        ActionListener, ComponentListener {
    private JToggleButton titleButton;
    private JComponent childComponent;

    public SelectableExpandablePanel(JComponent child) {
        this(child, null, null);
    }

    public SelectableExpandablePanel(JComponent child, String title) {
        this(child, title, null);
    }

    public SelectableExpandablePanel(JComponent child, String title,
            String tooltip) {
        super();

        if (child == null) {
            throw new IllegalArgumentException("Child component cannot be null");
        }

        childComponent = child;

        titleButton = new JToggleButton();
        titleButton.setText(title);
        titleButton.addActionListener(this);
        titleButton.setPreferredSize(new Dimension(getSize().width, titleButton
                .getPreferredSize().height));
        titleButton.setToolTipText(tooltip);

        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        setPreferredSize(new Dimension(childComponent.getPreferredSize().width,
                titleButton.getPreferredSize().height));
        setSize(new Dimension(childComponent.getPreferredSize().width,
                titleButton.getPreferredSize().height));

        add(titleButton);

        this.addComponentListener(this);
    }

    public void actionPerformed(ActionEvent e) {
        if (titleButton.isSelected()) {
            add(childComponent);
            setSize(new Dimension(childComponent.getPreferredSize().width,
                    titleButton.getPreferredSize().height
                            + childComponent.getPreferredSize().height));

        } else {
            remove(childComponent);
            setSize(new Dimension(childComponent.getPreferredSize().width,
                    titleButton.getPreferredSize().height));
        }

        invalidate();
        revalidate();
    }

    public void componentHidden(ComponentEvent arg0) {
        // Do nothing
    }

    public void componentMoved(ComponentEvent arg0) {
        // Do nothing
    }

    public void componentResized(ComponentEvent arg0) {
        titleButton.setSize(this.getWidth(),
                titleButton.getPreferredSize().height);
    }

    public void componentShown(ComponentEvent arg0) {
        // Do nothing
    }

    public static void main(String[] args) {
        JScrollPane scrollPane = new JScrollPane();

        // These panels simulates a complex, multi-line configuration panel.
        JPanel testPanel = new JPanel();
        testPanel.setLayout(new BoxLayout(testPanel, BoxLayout.Y_AXIS));
        testPanel.add(new JLabel("Test JLabel"));
        testPanel.add(new JLabel("Test JLabel 2"));
        testPanel.add(new JLabel("Test JLabel 3"));

        JPanel testPanel2 = new JPanel();
        testPanel2.setLayout(new BoxLayout(testPanel2, BoxLayout.Y_AXIS));
        testPanel2.add(new JLabel("Test JLabel"));
        testPanel2.add(new JLabel("Test JLabel 2"));
        testPanel2.add(new JLabel("Test JLabel 3"));

        JPanel testPanel3 = new JPanel();
        testPanel3.setLayout(new BoxLayout(testPanel3, BoxLayout.Y_AXIS));
        testPanel3.add(new JLabel("Test JLabel"));
        testPanel3.add(new JLabel("Test JLabel 2"));
        testPanel3.add(new JLabel("Test JLabel 3"));

        // This panel simulates the panel that will contain each of the
        // SelectableExpandablePanels.
        JPanel testHolder = new JPanel();
        testHolder.setLayout(new BoxLayout(testHolder, BoxLayout.Y_AXIS));
        testHolder.add(new SelectableExpandablePanel(testPanel, "Test"));
        testHolder.add(new SelectableExpandablePanel(testPanel2, "Test 2"));
        testHolder.add(new SelectableExpandablePanel(testPanel3, "Test 3"));

        // We add the test holder to the scroll pane. The intention is that if
        // the expansion is too big to fit, the holding JFrame won't expand, but
        // the scroll pane will get scroll bars to let the user scroll up and
        // down through the toggle buttons and any enabled items.
        scrollPane.setViewportView(testHolder);

        JFrame testFrame = new JFrame("Expandable Panel Test");
        testFrame.getContentPane().add(scrollPane);
        testFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        testFrame.pack();
        testFrame.setVisible(true);
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
Thomas Owens
  • 114,398
  • 98
  • 311
  • 431
  • [DYM](http://stackoverflow.com/a/8166223/714968) or [DYM](http://stackoverflow.com/a/11261110/714968), everything depends of what, how, when is JFrames childs (if yes or not) are resizable – mKorbel Apr 21 '15 at 19:15
  • @mKorbel Let me look at those. Looking quickly, it looks like using setVisible instead of adding or removing would likely be a better bet. I need to dig deeper for the scroll pane issues and width issues. – Thomas Owens Apr 21 '15 at 19:23
  • using setVisible instead of adding or removing would likely be a better ---> doesn't matter inside JScrollPane, is about my question to you – mKorbel Apr 21 '15 at 19:24
  • @mKorbel I don't know what you're asking. The child panels are resizable, yes. – Thomas Owens Apr 21 '15 at 19:25
  • everything depends of what, how, when is (what do you expects) JFrames childs (if yes or not) are resizable, then answer is easier – mKorbel Apr 21 '15 at 19:27
  • note JScollPane don't know returns its getPreferredSize back to container, LayoutManager uses its defaults, have to override of getPreferredSize or there isn't to setPreferredSize against any rulles or good habits – mKorbel Apr 21 '15 at 19:33
  • @mKorbel That may explain what I'm seeing. After making the changes as suggested by copeg, most of everything is working as I envisioned. However, the panel is only the width of the button. When the child component is wide, the panel needs to be as wide as the child component to eliminate horizontal scrolling. – Thomas Owens Apr 21 '15 at 19:35
  • [BoxLayout accepts all three limits, min, max and preferedSize](http://stackoverflow.com/a/9258934/714968), this is one of things that I missing in camickrs answer here – mKorbel Apr 21 '15 at 19:41

2 Answers2

2

Don't try to manage the sizes yourself:

//titleButton.setPreferredSize(new Dimension(getSize().width, titleButton.getPreferredSize().height));
 titleButton.setToolTipText(tooltip);

 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
//setPreferredSize(new Dimension(childComponent.getPreferredSize().width, titleButton.getPreferredSize().height));
//setSize(new Dimension(childComponent.getPreferredSize().width, titleButton.getPreferredSize().height));

Also, get rid of the setSize() code in the ActionListener. This will be ignored anyway as the layout manager will determine the size.

The scrollbars will appear when the preferred size of the panel is greater than the size of the scrollpane. If you hardcode the preferred size then you default the purpose of the layout manager and the preferred size won't change as you add/remove components.

Note for something like this I generally use a BorderLayout. Put the button in the PAGE_START and the other panel in the CENTER. The components will automatically fill the space available.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • Could you address the preferred method of sizing the scroll pane? For now, it's in my main method. I think I'll run into this issue when I use this component, though. If I throw it into a scroll pane like I did in the sample main method, the scroll pane will be the width of the buttons, which are much narrower than the child components in some cases. I need to force the scroll pane to be the width of the widest child component. What would be the best approach to handle this? – Thomas Owens Apr 21 '15 at 19:41
  • 1
    @ThomasOwens, If you need to manage the preferred size of your panel, then override the `getPreferredSize()` method of your class. This way the preferred size will by calculated dynamically every time the layout manager is invoked. – camickr Apr 21 '15 at 20:01
2
  1. Remove all the setSize/setPreferredSize calls and let the LayoutManager do its thing.
  2. To allow the JButtons to fill the width of the panel, you can use a BorderLayout (for instance, add the button to CENTER, and the child container to SOUTH and remove all those setSize values to let the LayoutManager handle it).
copeg
  • 8,290
  • 19
  • 28
  • This is perfect. I think. I need to test more, but this looks like the functionality I need. – Thomas Owens Apr 21 '15 at 19:26
  • 1
    Don't use setPreferredSize(), that is the job of the layout manager. – camickr Apr 21 '15 at 19:31
  • The only issue with this is that if I remove the setSize/setPreferredSize, the SelectableExpandablePanel's size is only the width of the buttons. The width needs to be the width of the widest child component, as I'd like to prevent horizontal scrolling. Aside from that, everything is working well. – Thomas Owens Apr 21 '15 at 19:32
  • agree with notice about setPreferredSize(), but another situations will be for JScrollpane (not mentioned in his answer) – mKorbel Apr 21 '15 at 19:35