001package jmri.util.swing;
002
003import java.awt.*;
004import javax.swing.JScrollPane;
005import javax.swing.SwingUtilities;
006
007/**
008 * FlowLayout subclass that fully supports wrapping of components.
009 */
010public class WrapLayout extends FlowLayout {
011
012    /**
013     * Constructs a new <code>WrapLayout</code> with a left alignment and a
014     * default 5-unit horizontal and vertical gap.
015     */
016    public WrapLayout() {
017        super();
018    }
019
020    /**
021     * Constructs a new <code>WrapLayout</code> with the specified alignment and
022     * a default 5-unit horizontal and vertical gap.
023     * The value of the alignment argument must be one of
024     * <code>FlowLayout.CENTER</code>, <code>FlowLayout.LEFT</code>, or
025     * <code>FlowLayout.RIGHT</code>.
026     *
027     * @param align the alignment value
028     */
029    public WrapLayout(int align) {
030        super(align);
031    }
032
033    /**
034     * Creates a new WrapLayout with the indicated alignment and the
035     * indicated horizontal and vertical gaps.
036     * <p>
037     * The value of the alignment argument must be one of
038     * <code>FlowLayout.CENTER</code>, <code>FlowLayout.LEFT</code>, or
039     * <code>FlowLayout.RIGHT</code>.
040     *
041     * @param align the alignment value
042     * @param hgap  the horizontal gap between components
043     * @param vgap  the vertical gap between components
044     */
045    public WrapLayout(int align, int hgap, int vgap) {
046        super(align, hgap, vgap);
047    }
048
049    /**
050     * Returns the preferred dimensions for this layout given the
051     * <i>visible</i> components in the specified target container.
052     *
053     * @param target the component which needs to be laid out
054     * @return the preferred dimensions to lay out the subcomponents of the
055     *         specified container
056     */
057    @Override
058    public Dimension preferredLayoutSize(Container target) {
059        return layoutSize(target, true);
060    }
061
062    /**
063     * Returns the minimum dimensions needed to layout the <i>visible</i>
064     * components contained in the specified target container.
065     *
066     * @param target the component which needs to be laid out
067     * @return the minimum dimensions to lay out the subcomponents of the
068     *         specified container
069     */
070    @Override
071    public Dimension minimumLayoutSize(Container target) {
072        Dimension minimum = layoutSize(target, false);
073        minimum.width -= (getHgap() + 1);
074        return minimum;
075    }
076
077    /**
078     * Returns the minimum or preferred dimension needed to layout the target
079     * container.
080     *
081     * @param target    target to get layout size for
082     * @param preferred should preferred size be calculated
083     * @return the dimension to layout the target container
084     */
085    private Dimension layoutSize(Container target, boolean preferred) {
086        synchronized (target.getTreeLock()) {
087            //  Each row must fit with the width allocated to the containter.
088            //  When the container width = 0, the preferred width of the container
089            //  has not yet been calculated so lets ask for the maximum.
090
091            Container container = target;
092
093            while (container.getSize().width == 0 && container.getParent() != null) {
094                container = container.getParent();
095            }
096
097            int targetWidth = container.getSize().width;
098
099            if (targetWidth == 0) {
100                targetWidth = Integer.MAX_VALUE;
101            }
102
103            int hgap = getHgap();
104            int vgap = getVgap();
105            Insets insets = target.getInsets();
106            int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
107            int maxWidth = targetWidth - horizontalInsetsAndGap;
108
109            //  Fit components into the allowed width
110            Dimension dim = new Dimension(0, 0);
111            int rowWidth = 0;
112            int rowHeight = 0;
113
114            int nmembers = target.getComponentCount();
115
116            for (int i = 0; i < nmembers; i++) {
117                Component m = target.getComponent(i);
118
119                if (m.isVisible()) {
120                    Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
121
122                    //  Can't add the component to current row. Start a new row.
123                    if (rowWidth + d.width > maxWidth) {
124                        addRow(dim, rowWidth, rowHeight);
125                        rowWidth = 0;
126                        rowHeight = 0;
127                    }
128
129                    //  Add a horizontal gap for all components after the first
130                    if (rowWidth != 0) {
131                        rowWidth += hgap;
132                    }
133
134                    rowWidth += d.width;
135                    rowHeight = Math.max(rowHeight, d.height);
136                }
137            }
138
139            addRow(dim, rowWidth, rowHeight);
140
141            dim.width += horizontalInsetsAndGap;
142            dim.height += insets.top + insets.bottom + vgap * 2;
143
144            // When using a scroll pane or the DecoratedLookAndFeel we need to
145            // make sure the preferred size is less than the size of the
146            // target containter so shrinking the container size works
147            // correctly. Removing the horizontal gap is an easy way to do this.
148            Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
149
150            if (scrollPane != null && target.isValid()) {
151                dim.width -= (hgap + 1);
152            }
153
154            return dim;
155        }
156    }
157
158    /**
159     *  A new row has been completed. Use the dimensions of this row
160     *  to update the preferred size for the container.
161     *
162     *  @param dim update the width and height when appropriate
163     *  @param rowWidth the width of the row to add
164     *  @param rowHeight the height of the row to add
165     */
166    private void addRow(Dimension dim, int rowWidth, int rowHeight) {
167        dim.width = Math.max(dim.width, rowWidth);
168
169        if (dim.height > 0) {
170            dim.height += getVgap();
171        }
172
173        dim.height += rowHeight;
174    }
175
176}