2

So basically if I put JPanels inside a JPanel that uses GridBagLayout and I restrict the size with setPreferredSize, eventually it reaches a point where it can't hold all of them, and it exhibits the behavior shown in the attached picture:

enter image description here

I'm making an accordion. This is just an example to showcase the problem I'm having. Each part of the accordion can open individually and they're of arbitrary size and get added on the fly. Its easy enough to get the heights of all the individual panels and compare them against the total height, but when too many are added it exhibits the crunching behavior I've shown. This also shrinks the heights so its much more difficult to determine when the crunching has happened. I would have to cache heights and somehow pre-calculate the heights of the new parts getting added. The end goal is to remove older panels when a new panel is added and there isn't enough room for it.

Is there an easy way to determine what height something would be if it weren't constrained, or maybe a supported way to detect when such crunching has is happening (so I can quickly thin it out before it gets painted again)? An option that makes GridBagLayout behave like some other layouts and overflow into hammerspace instead of compressing would work too.

Code for example:

import java.awt.*;
import java.awt.event.*;
import javaisms.out;
import javax.swing.*;

public class FoldDrag extends JLayeredPane {
    public TexturedPanel backingPanel = new TexturedPanel(new GridBagLayout(),"data/gui/grayerbricks.png");
    static JPanel windowbase=new JPanel();
    static JPanel restrictedpanel=new JPanel(new GridBagLayout());

    GridBagConstraints gbc = new GridBagConstraints();

    public FoldDrag() {
        JButton addpan  = new JButton("Add things");
        windowbase.add(addpan);
        windowbase.add(restrictedpanel);
        restrictedpanel.setBackground(Color.red);
        restrictedpanel.setPreferredSize(new Dimension(200,200));
        gbc.weighty=1;
        gbc.weightx=1;
        gbc.gridx=0;
        gbc.gridy=0;
        gbc.gridheight=1;
        gbc.gridwidth=1;
        gbc.fill=GridBagConstraints.HORIZONTAL;
        addpan.addActionListener(new ActionListener() {
            int number=0;
            @Override
            public void actionPerformed(ActionEvent e)
            {
                number++;
                gbc.gridy=number;
                JPanel tmppanel = new JPanel();
                tmppanel.setPreferredSize(new Dimension(100,30));
                if(number%3==0)
                    tmppanel.setBackground(Color.blue);
                if(number%3==1)
                    tmppanel.setBackground(Color.yellow);
                if(number%3==2)
                    tmppanel.setBackground(Color.green);
                restrictedpanel.add(tmppanel,gbc);
                restrictedpanel.validate();
            }
        });
        windowbase.setVisible(true);
    }
    private static void createAndShowUI() {
        JFrame frame = new JFrame("DragLabelOnLayeredPane");
        frame.getContentPane().add(windowbase);
        FoldDrag thedrag=new FoldDrag();
        windowbase.add(thedrag);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(300,300));
        frame.pack();
        frame.setResizable(true);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        out.active=true;
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                createAndShowUI();
            }
        });
    }
}

EDIT: Seems I didn't describe my version of the accordion very well. Here's a link.

gunfulker
  • 678
  • 6
  • 23
  • 1
    The panels are resorting to their minimum size when they are no longer able to use their preferred size, because there is not enough room to honor it. You might also want to have a read through [Should I avoid the use of set(Preferred|Maximum|Minimum)Size methods in Java Swing?](http://stackoverflow.com/questions/7229226/should-i-avoid-the-use-of-setpreferredmaximumminimumsize-methods-in-java-swi) – MadProgrammer Sep 03 '15 at 06:17
  • I understand whats happening and why, I need to detect *when* its happening. I need to use the setsize functions because of the contained nature of the accordion. The panel containing everything must never get any larger or smaller, no matter how large or small the contents are. The inside panels must take up the entire width of the containing panel and need have specific heights (otherwise the textured background looks broken), even if they lack the contents to stretch them to that size. – gunfulker Sep 03 '15 at 06:38
  • The you should consider writing your own layout manager – MadProgrammer Sep 03 '15 at 06:41
  • `reinventwheel(){ if(minor_problem()==true) reinventwheel(); }` – gunfulker Sep 03 '15 at 06:58
  • 1
    You have to consider the fact that you have a specific layout requirement which doesn't fit into the requirements of the other layout managers, you could continue applying hacks, like using `setSize` (which would only be undone by the layout manager anyway) and `setPreferred/Minimum/Maximum` size, or you could bite the bullet and design an actual layout manager that meets you particular, specialty needs. Of course, the choice is yours, but I know what I would do. Also, in your case `minor_problem()` is going to return `false`, because it's not... – MadProgrammer Sep 03 '15 at 07:04

3 Answers3

5

You have particular requirement which may be better served through the use of it's layout manager. This provides you the ability to control every aspect of the layout without the need to resort to hacks or "work arounds" which never quite work or have bizarre side effects

Accordion Layout

public class AccordionLayout implements LayoutManager {

    // This "could" be controlled by constraints, but that would assume
    // that more then one component could be expanded at a time
    private Component expanded;

    public void setExpanded(Component expanded) {
        this.expanded = expanded;
    }

    public Component getExpanded() {
        return expanded;
    }

    @Override
    public void addLayoutComponent(String name, Component comp) {
    }

    @Override
    public void removeLayoutComponent(Component comp) {
    }

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        Dimension size = minimumLayoutSize(parent);
        if (expanded != null) {
            size.height -= expanded.getMinimumSize().height;
            size.height += expanded.getPreferredSize().height;
        }

        return size;
    }

    @Override
    public Dimension minimumLayoutSize(Container parent) {
        int height = 0;
        int width = 0;
        for (Component comp : parent.getComponents()) {
            width = Math.max(width, comp.getPreferredSize().width);
            height += comp.getMinimumSize().height;
        }
        return new Dimension(width, height);
    }

    @Override
    public void layoutContainer(Container parent) {

        Insets insets = parent.getInsets();
        int availableHeight = parent.getHeight() - (insets.top + insets.bottom);
        int x = insets.left;
        int y = insets.top;

        int maxSize = 0;
        Dimension minSize = minimumLayoutSize(parent);
        if (expanded != null) {
            minSize.height -= expanded.getMinimumSize().height;
            // Try an honour the preferred size the expanded component...
            maxSize = Math.max(expanded.getPreferredSize().height, availableHeight - minSize.height);
        }

        int width = parent.getWidth() - (insets.left + insets.right);
        for (Component comp : parent.getComponents()) {
            if (expanded != comp) {
                comp.setSize(width, comp.getMinimumSize().height);
            } else {
                comp.setSize(width, maxSize);
            }
            comp.setLocation(x, y);
            y += comp.getHeight();
        }

    }

}

And the runnable example...

This goes to the enth degree, creating a specialised component to act as each "fold", but this just reduces the complexity of the API from the outside, meaning, you just need to think about the title and the content and let the rest of the API take care of itself

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.LineBorder;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private AccordionLayout layout;

        public TestPane() {
            layout = new AccordionLayout();
            setLayout(layout);

            AccordionListener listener = new AccordionListener() {
                @Override
                public void accordionSelected(Component comp) {
                    layout.setExpanded(comp);
                    revalidate();
                    repaint();
                }
            };

            Color colors[] = {Color.RED, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW};
            String titles[] = {"Red", "Blue", "Cyan", "Green", "Magenta", "Orange", "Pink", "Yellow"};
            for (int index = 0; index < colors.length; index++) {
                AccordionPanel panel = new AccordionPanel(titles[index], new ContentPane(colors[index]));
                panel.setAccordionListener(listener);
                add(panel);
            }
        }

    }

    public class ContentPane extends JPanel {

        public ContentPane(Color background) {
            setBackground(background);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 100);
        }

    }

    public interface AccordionListener {

        public void accordionSelected(Component comp);

    }

    public class AccordionPanel extends JPanel {

        private JLabel title;
        private JPanel header;
        private Component content;

        private AccordionListener accordionListener;

        public AccordionPanel() {
            setLayout(new BorderLayout());

            title = new JLabel("Title");

            header = new JPanel(new FlowLayout(FlowLayout.LEADING));
            header.setBackground(Color.GRAY);
            header.setBorder(new LineBorder(Color.BLACK));
            header.add(title);
            add(header, BorderLayout.NORTH);

            header.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    AccordionListener listener = getAccordionListener();
                    if (listener != null) {
                        listener.accordionSelected(AccordionPanel.this);
                    }
                }
            });
        }

        public AccordionPanel(String title) {
            this();
            setTitle(title);
        }

        public AccordionPanel(String title, Component content) {
            this(title);
            setContentPane(content);
        }

        public void setAccordionListener(AccordionListener accordionListener) {
            this.accordionListener = accordionListener;
        }

        public AccordionListener getAccordionListener() {
            return accordionListener;
        }

        public void setTitle(String text) {
            title.setText(text);
            revalidate();
        }

        public String getText() {
            return title.getText();
        }

        public void setContentPane(Component content) {
            if (this.content != null) {
                remove(this.content);
            }

            this.content = content;
            if (this.content != null) {
                add(this.content);
            }
            revalidate();
        }

        public Component getContent() {
            return content;
        }

        @Override
        public Dimension getMinimumSize() {
            return header.getPreferredSize();
        }

        @Override
        public Dimension getPreferredSize() {
            Dimension size = content != null ? content.getPreferredSize() : super.getPreferredSize();
            Dimension min = getMinimumSize();
            size.width = Math.max(min.width, size.width);
            size.height += min.height;
            return size;
        }

    }

    public class AccordionLayout implements LayoutManager {

        // This "could" be controled by constraints, but that would assume
        // that more then one component could be expanded at a time
        private Component expanded;

        public void setExpanded(Component expanded) {
            this.expanded = expanded;
        }

        public Component getExpanded() {
            return expanded;
        }

        @Override
        public void addLayoutComponent(String name, Component comp) {
        }

        @Override
        public void removeLayoutComponent(Component comp) {
        }

        @Override
        public Dimension preferredLayoutSize(Container parent) {
            Dimension size = minimumLayoutSize(parent);
            if (expanded != null) {
                size.height -= expanded.getMinimumSize().height;
                size.height += expanded.getPreferredSize().height;
            }

            return size;
        }

        @Override
        public Dimension minimumLayoutSize(Container parent) {
            int height = 0;
            int width = 0;
            for (Component comp : parent.getComponents()) {
                width = Math.max(width, comp.getPreferredSize().width);
                height += comp.getMinimumSize().height;
            }
            return new Dimension(width, height);
        }

        @Override
        public void layoutContainer(Container parent) {

            Insets insets = parent.getInsets();
            int availableHeight = parent.getHeight() - (insets.top + insets.bottom);
            int x = insets.left;
            int y = insets.top;

            int maxSize = 0;
            Dimension minSize = minimumLayoutSize(parent);
            if (expanded != null) {
                minSize.height -= expanded.getMinimumSize().height;
                // Try an honour the preferred size the expanded component...
                maxSize = Math.max(expanded.getPreferredSize().height, availableHeight - minSize.height);
            }

            int width = parent.getWidth() - (insets.left + insets.right);
            for (Component comp : parent.getComponents()) {
                if (expanded != comp) {
                    comp.setSize(width, comp.getMinimumSize().height);
                } else {
                    comp.setSize(width, maxSize);
                }
                comp.setLocation(x, y);
                y += comp.getHeight();
            }

        }

    }

}

Now, if you're really up for a challenge, you could use something a animated layout proxy and do something like...

Animated Layout

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • [This](http://i.imgur.com/oKUuF3f.webm) is what it does. I think I didn't my modified accordion very well. My "work around" works (so far). Tonight I'll see if I can understand your solution well enough to turn it into a robust fix. – gunfulker Sep 03 '15 at 22:38
  • The only thing I wasn't sure about was, should, if nothing is selected, the "title" sections fill the available space. The current implementation doesn't, it just uses the minimum size of the component (in this example, the title pane) to layout each fold, but that's a design choice you can make – MadProgrammer Sep 03 '15 at 23:31
  • I think I get it now. I'm just confused about when these `LayoutManager` methods get used automatically. [As you can see in my accordion](http://i.imgur.com/oKUuF3f.webm) it has 'windowshade' buttons and close buttons. It is also possible to add new panels. I'm assuming adding a panel will trigger the `LayoutManager` automatically, but will changing Minimum/maximum sizes of members prompt it to re-evaluate using `layoutContainer`? If I make the individual panels resizable by dragging the edges, will I need to activate the `LayoutManager` with every time `mouseDragged()`? – gunfulker Sep 04 '15 at 06:03
  • Also you said "This "could" be controled by constraints, but that would assume that more then one component could be expanded at a time". More then one component CAN be expanded at a time, so can you elaborate about how constraints would help? – gunfulker Sep 04 '15 at 06:05
  • For the constraints, you need a class which, at least, contains a "expanded" property. You will need to supply methods that allow a caller to get and set the constraints for a given component. When you calculate the layout, you need to get the constraints and determine which components are expanded and which are not. This gets a little more complicated as you may need to create constraints for components when they are added (and remove them when they are removed), which may require you to use `LayoutManager2` which provides more options for object based constraints) – MadProgrammer Sep 04 '15 at 06:08
  • Calling `revalidate` will cause the layout to be recalculated in most cases (you usually follow this up with a `repaint`). When you "drag" a component out, you would only need to `revalidate` the container once for the remove, when you drop it back in, you would need to `revalidate` the container again once it's been re-added – MadProgrammer Sep 04 '15 at 06:09
  • Got it mostly working. While the example doesn't match what I was doing, making a `LayoutManager` is clearly the right solution. – gunfulker Sep 04 '15 at 18:43
  • SwingLabs SwingX library has a collapsible panel and a vertical layout which could solve your issues – MadProgrammer Sep 05 '15 at 01:27
  • I came back to say you weren't just right, you were extremely right. LayoutManager isn't an alternative solution, it is far easier and cleaner for any layout that is remotely dynamic. – gunfulker Sep 19 '15 at 01:37
  • @gunfulker That makes a nice change ;) – MadProgrammer Sep 19 '15 at 02:29
2

The end goal is to remove older panels when a new panel is added and there isn't enough room for it

I would guess that after you add a panel you compare the preferred height with the actual height. When the preferred height is greater you have a problem and you remove components as required.

So then the next problem is to use a layout manager that doesn't change the heights of the panels. This can still be done with the GridBagLayout. You just need to override the getMinimumSize() method to return the getPreferredSize() Dimension.

Each part of the accordion can open individually and they're of arbitrary size and get added on the fly

You might want to consider using the Relative Layout. You can add components whose preferred size will be respected. So you will be able to check when the preferred height is greater than the actual height.

Then you can also add components that will be sized based on the amount of space left in the panel. These would be your expanding panels.

So in your example you example when you expand an item you could configure that component to take up the entire space available. If you expand two items then they would each get half the space available.

Maybe something like this:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ExpandingPanel extends JPanel
{
    private JPanel expanding;

    public ExpandingPanel(String text, Color color)
    {
        setLayout( new BorderLayout() );

        JButton button = new JButton( text );
        add(button, BorderLayout.NORTH);

        expanding = new JPanel();
        expanding.setBackground( color );
        expanding.setVisible( false );
        add(expanding, BorderLayout.CENTER);

        button.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                expanding.setVisible( !expanding.isVisible() );

                Container parent = ExpandingPanel.this.getParent();
                LayoutManager2 layout = (LayoutManager2)parent.getLayout();

                if (expanding.isVisible())
                    layout.addLayoutComponent(ExpandingPanel.this, new Float(1));
                else
                    layout.addLayoutComponent(ExpandingPanel.this, null);

                revalidate();
                repaint();
            }
        });
    }

    private static void createAndShowGUI()
    {
        RelativeLayout rl = new RelativeLayout(RelativeLayout.Y_AXIS);
        rl.setFill( true );

        JPanel content = new JPanel( rl );
        content.add( new ExpandingPanel("Red", Color.RED) );
        content.add( new ExpandingPanel("Blue", Color.BLUE) );
        content.add( new ExpandingPanel("Green", Color.GREEN) );

        JFrame frame = new JFrame("Expanding Panel");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add( content);
        frame.setLocationByPlatform( true );
        frame.setSize(200, 300);
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}
camickr
  • 321,443
  • 19
  • 166
  • 288
  • I'm going to try the bit about overriding `getMinimumSize()` tonight. I uploaded [this](http://i.imgur.com/oKUuF3f.webm) to show what the actual thing looks like. I've patched the crunching problem for now using the solution I posted, but everyone's telling me I need a more robust method and the overriding sounds like it might work and be easy. – gunfulker Sep 03 '15 at 22:29
0

You can tell something is "crunched" when panel.getPreferredSize().height != panel.getHeight() and panel.getPreferredSize().width != panel.getWidth()

gunfulker
  • 678
  • 6
  • 23