001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.awt.Component;
005import java.awt.Container;
006import java.awt.Dimension;
007import java.awt.FlowLayout;
008import java.awt.Insets;
009import java.util.function.Function;
010
011/**
012 * This is an extension of the flow layout that preferes wrapping the text instead of increasing the component width
013 * when there is not enough space.
014 * <p>
015 * This allows for a better preffered size computation.
016 * It should be used in all places where a flow layout fills the full width of the parent container.
017 * <p>
018 * This does not support baseline alignment.
019 * @author Michael Zangl
020 * @since 10622
021 */
022public class MultiLineFlowLayout extends FlowLayout {
023    /**
024     * Same as {@link FlowLayout#FlowLayout()}
025     */
026    public MultiLineFlowLayout() {
027        super();
028    }
029
030    /**
031     * Same as {@link FlowLayout#FlowLayout(int, int, int)}
032     * @param align Alignment
033     * @param hgap horizontal gap
034     * @param vgap vertical gap
035     */
036    public MultiLineFlowLayout(int align, int hgap, int vgap) {
037        super(align, hgap, vgap);
038    }
039
040    /**
041     * Same as {@link FlowLayout#FlowLayout(int)}
042     * @param align Alignment
043     */
044    public MultiLineFlowLayout(int align) {
045        super(align);
046    }
047
048    @Override
049    public Dimension preferredLayoutSize(Container target) {
050        return getLayoutSize(target, Component::getPreferredSize);
051    }
052
053    @Override
054    public Dimension minimumLayoutSize(Container target) {
055        return getLayoutSize(target, Component::getMinimumSize);
056    }
057
058    private Dimension getLayoutSize(Container target, Function<Component, Dimension> baseSize) {
059        synchronized (target.getTreeLock()) {
060            int outerWidth = getWidthOf(target);
061
062            Insets insets = target.getInsets();
063            int containerWidth = outerWidth - insets.left - insets.right - getHgap() * 2;
064
065            int x = 0;
066            int totalHeight = insets.top + insets.bottom + getVgap() * 2;
067            int rowHeight = 0;
068            for (int i = 0; i < target.getComponentCount(); i++) {
069                Component child = target.getComponent(i);
070                if (!child.isVisible()) {
071                    continue;
072                }
073                Dimension size = baseSize.apply(child);
074                if (x != 0) {
075                    x += getHgap();
076                }
077                x += size.width;
078                if (x > containerWidth) {
079                    totalHeight += rowHeight + getVgap();
080                    rowHeight = 0;
081                    x = 0;
082                }
083
084                rowHeight = Math.max(rowHeight, size.height);
085            }
086            totalHeight += rowHeight;
087
088            return new Dimension(outerWidth, totalHeight);
089        }
090    }
091
092    private static int getWidthOf(Container target) {
093        Container current = target;
094        while (current.getWidth() == 0 && current.getParent() != null) {
095            current = current.getParent();
096        }
097        int width = current.getWidth();
098        if (width == 0) {
099            return Integer.MAX_VALUE;
100        } else {
101            return width;
102        }
103    }
104
105    @Override
106    public String toString() {
107        return "MultiLineFlowLayout [align=" + getAlignment() + ']';
108    }
109}