ResponsiveGridFlowLayout.java
package swingtree.layout;
import net.miginfocom.layout.LC;
import net.miginfocom.swing.MigLayout;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import swingtree.SwingTree;
import swingtree.UI;
import javax.swing.*;
import java.awt.*;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A flow layout arranges components in a directional flow, much
* like lines of text in a paragraph.
*/
public final class ResponsiveGridFlowLayout implements LayoutManager2 {
private static final int NUMBER_OF_COLUMNS = 12;
private static final Logger log = LoggerFactory.getLogger(ResponsiveGridFlowLayout.class);
private UI.HorizontalAlignment _alignmentCode;
private int _horizontalGapSize;
private int _verticalGapSize;
private boolean _alignOnBaseline;
/**
* Constructs a new {@code FlowLayout} with a centered alignment and a
* default 5-unit horizontal and vertical gap.
*/
public ResponsiveGridFlowLayout() {
this(UI.HorizontalAlignment.CENTER, 5, 5);
}
/**
* Constructs a new {@code FlowLayout} with the specified
* alignment and a default 5-unit horizontal and vertical gap.
* The value of the alignment argument must be one of
* {@code FlowLayout.LEFT}, {@code FlowLayout.RIGHT},
* {@code FlowLayout.CENTER}, {@code FlowLayout.LEADING},
* or {@code FlowLayout.TRAILING}.
*
* @param align the alignment value
*/
public ResponsiveGridFlowLayout(UI.HorizontalAlignment align) {
this(align, 5, 5);
}
/**
* Creates a new flow layout manager with the indicated alignment
* and the indicated horizontal and vertical gaps.
* <p>
* The value of the alignment argument must be one of
* {@link UI.HorizontalAlignment#LEFT}, {@link UI.HorizontalAlignment#RIGHT},
* {@link UI.HorizontalAlignment#CENTER}, {@link UI.HorizontalAlignment#LEADING},
* or {@link UI.HorizontalAlignment#TRAILING}.
*
* @param align the alignment value
* @param horizontalGapSize the horizontal gap between components
* and between the components and the
* borders of the {@code Container}
* @param verticalGapSize the vertical gap between components
* and between the components and the
* borders of the {@code Container}
*/
public ResponsiveGridFlowLayout(
UI.HorizontalAlignment align,
int horizontalGapSize,
int verticalGapSize
) {
_alignmentCode = align;
_horizontalGapSize = horizontalGapSize;
_verticalGapSize = verticalGapSize;
}
/**
* Gets the alignment for this layout.
* Possible values are {@link UI.HorizontalAlignment#LEFT},
* {@link UI.HorizontalAlignment#RIGHT}, {@link UI.HorizontalAlignment#CENTER},
* {@link UI.HorizontalAlignment#LEADING},
* or {@link UI.HorizontalAlignment#TRAILING}.
*
* @return the alignment value for this layout
* @see #setAlignment
*/
public UI.HorizontalAlignment getAlignment() {
return _alignmentCode;
}
/**
* Sets the alignment for this layout.
* Possible values are
* <ul>
* <li>{@link UI.HorizontalAlignment#LEFT}
* <li>{@link UI.HorizontalAlignment#RIGHT}
* <li>{@link UI.HorizontalAlignment#CENTER}
* <li>{@link UI.HorizontalAlignment#LEADING}
* <li>{@link UI.HorizontalAlignment#TRAILING}
* </ul>
*
* @param align one of the alignment values shown above
* @see #getAlignment()
*/
public void setAlignment(UI.HorizontalAlignment align) {
_alignmentCode = align;
}
/**
* Gets the horizontal gap between components
* and between the components and the borders
* of the {@code Container}
*
* @return the horizontal gap between components
* and between the components and the borders
* of the {@code Container}
* @see ResponsiveGridFlowLayout#setHorizontalGapSize(int)
*/
public int horizontalGapSize() {
return UI.scale(_horizontalGapSize);
}
/**
* Sets the horizontal gap between components and
* between the components and the borders of the
* {@code Container}.
*
* @param size the horizontal gap between components
* and between the components and the borders
* of the {@code Container}
* @see ResponsiveGridFlowLayout#horizontalGapSize()
*/
public void setHorizontalGapSize(int size) {
_horizontalGapSize = size;
}
/**
* Gets the vertical gap between components and
* between the components and the borders of the
* {@code Container}.
*
* @return the vertical gap between components
* and between the components and the borders
* of the {@code Container}
* @see ResponsiveGridFlowLayout#setVerticalGapSize(int)
*/
public int verticalGapSize() {
return UI.scale(_verticalGapSize);
}
/**
* Sets the vertical gap between components and between
* the components and the borders of the {@code Container}.
*
* @param size the vertical gap between components
* and between the components and the borders
* of the {@code Container}
* @see ResponsiveGridFlowLayout#verticalGapSize()
*/
public void setVerticalGapSize(int size) {
_verticalGapSize = size;
}
/**
* Sets whether or not components should be vertically aligned along their
* baseline. Components that do not have a baseline will be centered.
* The default is false.
*
* @param alignOnBaseline whether or not components should be
* vertically aligned on their baseline
*/
public void setAlignOnBaseline(boolean alignOnBaseline) {
this._alignOnBaseline = alignOnBaseline;
}
/**
* Returns true if components are to be vertically aligned along
* their baseline. The default is false.
*
* @return true if components are to be vertically aligned along
* their baseline
*/
public boolean getAlignOnBaseline() {
return _alignOnBaseline;
}
/**
* Adds the specified component to the layout.
* Not used by this class.
*
* @param name the name of the component
* @param comp the component to be added
*/
@Override
public void addLayoutComponent( String name, Component comp ) {
}
/**
* Removes the specified component from the layout.
* Not used by this class.
*
* @param comp the component to remove
* @see java.awt.Container#removeAll
*/
@Override
public void removeLayoutComponent( Component comp ) {
}
/**
* Returns the preferred dimensions for this layout given the
* <i>visible</i> components in the specified target container.
*
* @param target the container that needs to be laid out
* @return the preferred dimensions to lay out the
* subcomponents of the specified container
* @see java.awt.Container
* @see #minimumLayoutSize
* @see java.awt.Container#getPreferredSize
*/
@Override
public Dimension preferredLayoutSize( Container target ) {
return preferredLayoutSizeAtWidth(target, target.getWidth());
}
/**
* Returns the size this layout would like to have if its target container
* were {@code width} pixels wide.
* <p>
* This layout wraps its children into rows, which makes it a
* <i>width-for-height</i> layout: the height it needs is a function of the
* width it is given, and it cannot be expressed as a single fixed number.
* A parent container which itself decides the width of this layout's target
* must therefore ask this method rather than {@link #preferredLayoutSize(Container)},
* because at the time it asks, the target still has the width of the
* previous layout pass.
* <p>
* The returned width does not depend on {@code width}: it is the "ideal" width
* of a single row holding all children, raised to the explicitly set preferred
* width of the target if there is one and it is larger. Only the height varies
* with {@code width}.
* <p>
* Mind that the returned width is <b>not</b> necessarily the reference width
* against which the {@link ParentSizeClass} of the children is measured. That
* reference is the explicitly set preferred width whenever there is one, even
* when it is <i>smaller</i> than the ideal single row width — which is exactly
* how a nested grid keeps its own size categories usable without imposing its
* single row width on the grid it sits in. An explicitly set preferred size
* therefore acts as a lower bound on the returned size and, independently of
* that, as the reference width.
*
* @param target The container whose layout size should be computed.
* @param width The width the container is assumed to have, where a value of
* {@code 0} or less means that no width is known yet, in which
* case the children are assumed to fit into a single row.
* @return The preferred size of the given container at the given width.
*/
public Dimension preferredLayoutSizeAtWidth( Container target, int width ) {
synchronized (target.getTreeLock()) {
final Insets insets = target.getInsets();
final int hgap = UI.scale(_horizontalGapSize);
final int vgap = UI.scale(_verticalGapSize);
Dimension dim = _singleRowSize(target);
final int maxwidth = width - (insets.left + insets.right + hgap * 2);
if ( maxwidth > 0 ) {
// The container has a width, so we can determine how the children wrap:
final int referenceWidth = _referenceWidth(target, dim.width + insets.left + insets.right);
final int generalMaxWidth = referenceWidth - (insets.left + insets.right + hgap * 2);
final int nmembers = target.getComponentCount();
Cell[] cells = _createCells(target, nmembers, maxwidth, generalMaxWidth);
dim.height = _wrappedContentHeight(cells, maxwidth, hgap, vgap) + vgap * 2;
}
dim.width += insets.left + insets.right;
dim.height += insets.top + insets.bottom;
if ( target.isPreferredSizeSet() ) {
// An explicitly set preferred size is a lower bound, it is what the user asked for.
Dimension explicit = target.getPreferredSize();
dim.width = Math.max(dim.width, explicit.width);
dim.height = Math.max(dim.height, explicit.height);
}
return dim;
}
}
/**
* The size of all children laid out in a single row, without the insets of
* the target. This is the "ideal" size of this layout, which doubles as the
* reference width for the {@link ParentSizeClass} of the children.
*/
private Dimension _singleRowSize( Container target ) {
Dimension dim = new Dimension(0, 0);
int nmembers = target.getComponentCount();
boolean firstVisibleComponent = true;
boolean useBaseline = getAlignOnBaseline();
int maxAscent = 0;
int maxDescent = 0;
int hgap = UI.scale(_horizontalGapSize);
int vgap = UI.scale(_verticalGapSize);
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
Dimension d = m.getPreferredSize();
dim.height = Math.max(dim.height, d.height);
if (firstVisibleComponent) {
firstVisibleComponent = false;
} else {
dim.width += hgap;
}
dim.width += d.width;
if (useBaseline) {
int baseline = m.getBaseline(d.width, d.height);
if (baseline >= 0) {
maxAscent = Math.max(maxAscent, baseline);
maxDescent = Math.max(maxDescent, d.height - baseline);
}
}
}
}
if (useBaseline) {
dim.height = Math.max(maxAscent + maxDescent, dim.height);
}
dim.width += hgap * 2;
dim.height += vgap * 2;
return dim;
}
/**
* The width against which the {@link ParentSizeClass} of the children is
* measured. An explicitly set preferred width wins, because it is a
* deliberate statement about how wide this grid considers itself to be.
* Otherwise the ideal single row width is used.
* <p>
* Note that this deliberately avoids {@code target.getPreferredSize()} when
* no preferred size was set, because that would recurse back into this layout.
*/
private int _referenceWidth( Container target, int singleRowWidth ) {
if ( target.isPreferredSizeSet() ) {
int explicitWidth = target.getPreferredSize().width;
if ( explicitWidth > 0 )
return explicitWidth;
// A preferred width of zero or less is not a meaningful statement about
// how wide this grid is when it considers itself full. Taken literally
// it makes every size category degenerate, so we ignore it and fall back
// to the ideal single row width, exactly as if none had been set.
}
return singleRowWidth;
}
/**
* Walks the cells exactly like {@link #layoutContainer(Container)} does, but
* only accumulates how much vertical room the resulting rows occupy, without
* moving or resizing anything.
* <p>
* Note that this has to mirror how {@link #moveComponents} arrives at a row
* height, baseline alignment included. Otherwise the height promised here and
* the height actually laid out drift apart, and the difference is clipped.
*/
private int _wrappedContentHeight(
Cell[] cells, int maxwidth, int hgap, int vgap
) {
final boolean useBaseline = getAlignOnBaseline();
int x = 0, y = 0;
RowHeight row = new RowHeight();
for ( Cell cell : cells ) {
Component m = cell.component();
if (!m.isVisible())
continue;
Dimension d = _cellSize(cell, maxwidth);
if ( (x != 0) && ((x + d.width) > maxwidth) ) {
// The component does not fit into the current row, so a new one starts:
y += vgap + row.resolve(useBaseline);
x = 0;
row = new RowHeight();
}
if (x > 0)
x += hgap;
x += d.width;
row.add(m, d, useBaseline);
}
return y + row.resolve(useBaseline);
}
/**
* Accumulates the height of a single row of cells, both the plain way (the
* tallest child) and the baseline aligned way (the deepest ascent plus the
* deepest descent, which may come from two different children and can
* therefore exceed the height of any one of them).
*/
private static final class RowHeight
{
private int tallest = 0;
private int maxAscent = 0;
private int maxDescent = 0;
private int nonBaselineHeight = 0;
void add( Component m, Dimension d, boolean useBaseline ) {
tallest = Math.max(tallest, d.height);
if ( !useBaseline )
return;
int baseline = -1;
if ( d.width >= 0 && d.height >= 0 )
baseline = m.getBaseline(d.width, d.height);
if ( baseline >= 0 ) {
maxAscent = Math.max(maxAscent, baseline);
maxDescent = Math.max(maxDescent, d.height - baseline);
} else {
nonBaselineHeight = Math.max(nonBaselineHeight, d.height);
}
}
int resolve( boolean useBaseline ) {
if ( !useBaseline )
return tallest;
return Math.max(maxAscent + maxDescent, nonBaselineHeight);
}
}
/**
* The size a cell's component ends up with, which is its configured span
* width where a span applies, and its preferred size otherwise.
*/
private Dimension _cellSize( Cell cell, int maxwidth ) {
Dimension d = cell.component().getPreferredSize();
try {
d = _dimensionsFromCellConf(cell, maxwidth).orElse(d);
} catch (Exception e) {
log.error(SwingTree.get().logMarker(), "Error applying cell configuration", e);
}
return d;
}
/**
* Asks a component how tall it wants to be if it were {@code width} pixels
* wide. Only a nested {@link ResponsiveGridFlowLayout} can actually answer
* that question, every other component is asked for its preferred height.
* <p>
* Note that a nested grid is asked even when it has an explicitly set
* preferred size. Its rows still have to fit, so the explicit size acts as a
* lower bound rather than as a fixed height. Setting it is how a nested grid
* declares the width at which it considers itself "full", see
* {@link #preferredLayoutSizeAtWidth(Container, int)}.
*/
private static int _preferredHeightAtWidth( Component component, int width ) {
if ( component instanceof Container ) {
LayoutManager layout = ((Container) component).getLayout();
if ( layout instanceof ResponsiveGridFlowLayout )
return ((ResponsiveGridFlowLayout) layout)
.preferredLayoutSizeAtWidth((Container) component, width)
.height;
}
return component.getPreferredSize().height;
}
/**
* Returns the minimum dimensions needed to layout the <i>visible</i>
* components contained in the specified target container.
*
* @param target the container that needs to be laid out
* @return the minimum dimensions to lay out the
* subcomponents of the specified container
* @see #preferredLayoutSize
* @see java.awt.Container
* @see java.awt.Container#doLayout
*/
@Override
public Dimension minimumLayoutSize( Container target ) {
synchronized (target.getTreeLock()) {
boolean useBaseline = getAlignOnBaseline();
Dimension dim = new Dimension(0, 0);
int nmembers = target.getComponentCount();
int maxAscent = 0;
int maxDescent = 0;
boolean firstVisibleComponent = true;
int hgap = UI.scale(_horizontalGapSize);
int vgap = UI.scale(_verticalGapSize);
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
Dimension d = m.getMinimumSize();
dim.height = Math.max(dim.height, d.height);
if (firstVisibleComponent) {
firstVisibleComponent = false;
} else {
dim.width += hgap;
}
dim.width += d.width;
if (useBaseline) {
int baseline = m.getBaseline(d.width, d.height);
if (baseline >= 0) {
maxAscent = Math.max(maxAscent, baseline);
maxDescent = Math.max(maxDescent,
dim.height - baseline);
}
}
}
}
if (useBaseline) {
dim.height = Math.max(maxAscent + maxDescent, dim.height);
}
Insets insets = target.getInsets();
dim.width += insets.left + insets.right + hgap * 2;
dim.height += insets.top + insets.bottom + vgap * 2;
return dim;
}
}
/**
* Centers the elements in the specified row, if there is any slack.
*
* @param target the component which needs to be moved
* @param cells an array of cells, one for each component of the target
* @param x the x coordinate
* @param y the y coordinate
* @param width the width dimensions
* @param height the height dimensions
* @param rowStart the beginning of the row
* @param rowEnd the ending of the row
* @param useBaseline Whether or not to align on baseline.
* @param ascent Ascent for the components. This is only valid if
* useBaseline is true.
* @param descent Ascent for the components. This is only valid if
* useBaseline is true.
* @return actual row height
*/
private int moveComponents(
Container target, Cell[] cells,
int x, int y, int width, int height,
int rowStart, int rowEnd, boolean ltr,
boolean useBaseline,
int@Nullable [] ascent,
int@Nullable [] descent
) {
int hgap = UI.scale(_horizontalGapSize);
switch (_alignmentCode) {
case LEFT:
x += ltr ? 0 : width;
break;
case CENTER:
x += width / 2;
break;
case RIGHT:
x += ltr ? width : 0;
break;
case LEADING:
break;
case TRAILING:
x += width;
break;
case UNDEFINED:
break; // Should happen?
}
int maxAscent = 0;
int nonbaselineHeight = 0;
int baselineOffset = 0;
if (useBaseline) {
Objects.requireNonNull(ascent);
Objects.requireNonNull(descent);
int maxDescent = 0;
for (int i = rowStart; i < rowEnd; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
if (ascent[i] >= 0) {
maxAscent = Math.max(maxAscent, ascent[i]);
maxDescent = Math.max(maxDescent, descent[i]);
} else {
nonbaselineHeight = Math.max(m.getHeight(),
nonbaselineHeight);
}
}
}
height = Math.max(maxAscent + maxDescent, nonbaselineHeight);
baselineOffset = (height - maxAscent - maxDescent) / 2;
}
for (int i = rowStart; i < rowEnd; i++) {
Component m = target.getComponent(i);
if (m.isVisible()) {
Optional<FlowCellConf> optionalFlowCellConf = cells[i].flowCell();
boolean fillHeight = optionalFlowCellConf.map(FlowCellConf::fill).orElse(false);
UI.VerticalAlignment verticalAlignment = optionalFlowCellConf.map(FlowCellConf::verticalAlignment).orElse(UI.VerticalAlignment.CENTER);
int cy;
if (ascent != null && useBaseline && ascent[i] >= 0) {
cy = y + baselineOffset + maxAscent - ascent[i];
} else {
if (fillHeight) {
cy = y;
} else {
if ( verticalAlignment == UI.VerticalAlignment.TOP )
cy = y;
else if ( verticalAlignment == UI.VerticalAlignment.BOTTOM )
cy = y + height - m.getHeight();
else // centered:
cy = y + (height - m.getHeight()) / 2;
}
}
if (ltr) {
m.setLocation(x, cy);
} else {
m.setLocation(target.getWidth() - x - m.getWidth(), cy);
}
x += m.getWidth() + hgap;
if ( fillHeight ) {
m.setSize(m.getWidth(), height);
}
}
}
return height;
}
/**
* Lays out the container. This method lets each
* <i>visible</i> component take
* its preferred size by reshaping the components in the
* target container in order to satisfy the alignment of
* this layout manager.
*
* @param target the specified component being laid out
* @see Container
* @see java.awt.Container#doLayout
*/
@Override
public void layoutContainer(Container target) {
synchronized (target.getTreeLock()) {
final int hgap = UI.scale(_horizontalGapSize);
final int vgap = UI.scale(_verticalGapSize);
final Insets insets = target.getInsets();
final int maxwidth = target.getWidth() - (insets.left + insets.right + hgap * 2);
// Note that the reference width is deliberately derived the same way as
// in `preferredLayoutSizeAtWidth`, and not simply read from
// `getPreferredSize()`. Otherwise measuring and laying out could pick
// two different size categories for the very same container.
final int referenceWidth = _referenceWidth(
target,
_singleRowSize(target).width + insets.left + insets.right
);
final int generalMaxWidth = referenceWidth - (insets.left + insets.right + hgap * 2);
final int nmembers = target.getComponentCount();
int x = 0, y = insets.top + vgap;
int rowh = 0, start = 0;
Cell[] cells = _createCells(target, nmembers, maxwidth, generalMaxWidth);
boolean ltr = target.getComponentOrientation().isLeftToRight();
boolean useBaseline = getAlignOnBaseline();
int[] ascent = null;
int[] descent = null;
if (useBaseline) {
ascent = new int[nmembers];
descent = new int[nmembers];
}
for (int i = 0; i < nmembers; i++) {
Component m = cells[i].component();
if (m.isVisible()) {
Dimension d = _cellSize(cells[i], maxwidth);
m.setSize(d.width, d.height);
if (useBaseline ) {
Objects.requireNonNull(ascent);
Objects.requireNonNull(descent);
int baseline = m.getBaseline(d.width, d.height);
if (baseline >= 0) {
ascent[i] = baseline;
descent[i] = d.height - baseline;
} else {
ascent[i] = -1;
}
}
if ((x == 0) || ((x + d.width) <= maxwidth)) {
if (x > 0) {
x += hgap;
}
x += d.width;
rowh = Math.max(rowh, d.height);
} else {
rowh = moveComponents(
target, cells,
insets.left + hgap, y,
maxwidth - x, rowh, start, i, ltr,
useBaseline, ascent, descent
);
x = d.width;
y += vgap + rowh;
rowh = d.height;
start = i;
}
}
}
moveComponents(
target, cells,
insets.left + hgap, y, maxwidth - x, rowh,
start, nmembers, ltr, useBaseline, ascent, descent
);
}
}
private Cell[] _createCells(
Container target,
int nmembers,
int maxwidth,
int generalMaxWidth
) {
Cell[] cells = new Cell[nmembers];
AtomicInteger componentsInRow = new AtomicInteger(0);
double currentRowSize = 0;
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
Optional<Cell> optionalCell = Optional.empty();
double rowSizeIncrease = 0;
if (m instanceof JComponent) {
JComponent jc = (JComponent) m;
AddConstraint addConstraint = (AddConstraint) jc.getClientProperty(AddConstraint.class);
if (addConstraint instanceof FlowCell) {
FlowCell cell = (FlowCell) addConstraint;
optionalCell = cellFromCellConf(target, cell, jc, componentsInRow, maxwidth, generalMaxWidth);
rowSizeIncrease += optionalCell.flatMap(Cell::autoSpan)
.map(FlowCellSpanPolicy::cellsToFill)
.orElse(0);
}
}
if ( !optionalCell.isPresent() ) {
double prefComponentWidth = m.getPreferredSize().getWidth();
if ( maxwidth > 0 && prefComponentWidth > 0 ) {
rowSizeIncrease += NUMBER_OF_COLUMNS * prefComponentWidth / maxwidth;
}
}
cells[i] = optionalCell.orElse(new Cell(m, componentsInRow, null, null));
double newRowSize = currentRowSize + rowSizeIncrease;
if ( newRowSize < NUMBER_OF_COLUMNS ) {
// Still room in the row for new components...
componentsInRow.set(componentsInRow.get() + 1);
currentRowSize += rowSizeIncrease;
} else if ( Math.round(newRowSize) == NUMBER_OF_COLUMNS ) {
// We have a new row with no leftovers.
componentsInRow.set(componentsInRow.get() + 1);
componentsInRow = new AtomicInteger(0);
currentRowSize = 0;
} else if ( newRowSize > NUMBER_OF_COLUMNS ) {
// The row does not fit the new component. We need to start a new row.
componentsInRow = new AtomicInteger(1);
cells[i].setNumberOfComponents(componentsInRow);
currentRowSize = rowSizeIncrease; // We have a new row with the current component.
}
}
return cells;
}
/**
* Returns a string representation of this {@code FlowLayout}
* object and its values.
*
* @return a string representation of this layout
*/
@Override
public String toString() {
String str = "";
int hgap = UI.scale(_horizontalGapSize);
int vgap = UI.scale(_verticalGapSize);
switch (_alignmentCode) {
case LEFT:
str = ",align=left";
break;
case CENTER:
str = ",align=center";
break;
case RIGHT:
str = ",align=right";
break;
case LEADING:
str = ",align=leading";
break;
case TRAILING:
str = ",align=trailing";
break;
case UNDEFINED:
str = ",align=?";
break;
}
return getClass().getName() + "[horizontalGap=" + hgap + ",verticalGap=" + vgap + str + "]";
}
@Override
public void addLayoutComponent(Component comp, Object constraints) {
if (constraints instanceof AddConstraint) {
if (comp instanceof JComponent) {
JComponent jc = (JComponent) comp;
jc.putClientProperty(AddConstraint.class, constraints);
}
}
}
@Override
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
@Override
public float getLayoutAlignmentX(Container target) {
return 0;
}
@Override
public float getLayoutAlignmentY(Container target) {
return 0;
}
@Override
public void invalidateLayout(Container target) {
}
public Optional<Cell> cellFromCellConf(
Component parent,
FlowCell flowCell,
Component child,
AtomicInteger componentCounter,
int maxWidth,
int generalMaxWidth
) {
if ( maxWidth <= 0 ) {
return Optional.empty();
}
// How much preferred width the parent actually fills:
ParentSizeClass currentParentSizeCategory = ParentSizeClass.of(maxWidth, generalMaxWidth);
boolean shouldFillHeight = false;
LayoutManager childLayout = ( child instanceof JComponent ) ? ((JComponent) child).getLayout() : null;
if ( childLayout instanceof MigLayout ) {
Object layoutConstraints = ((MigLayout) childLayout).getLayoutConstraints();
// If the child has the "fill" or "filly" constraint, we should fill the height.
if ( layoutConstraints instanceof String ) {
String constraints = (String) layoutConstraints;
shouldFillHeight = constraints.contains("fill") || constraints.contains("filly");
} else if ( layoutConstraints instanceof LC ) {
LC lc = (LC) layoutConstraints;
shouldFillHeight = lc.isFillY();
}
}
Size parentSize = Size.of(parent.getWidth(), parent.getHeight());
FlowCellConf cellConf = flowCell.fetchConfig(NUMBER_OF_COLUMNS, parentSize, currentParentSizeCategory, shouldFillHeight);
Optional<FlowCellSpanPolicy> autoSpan = _findNextBestAutoSpan(cellConf, currentParentSizeCategory);
return autoSpan.map(autoCellSpanPolicy -> new Cell(child, componentCounter, autoCellSpanPolicy, cellConf));
}
private Optional<Dimension> _dimensionsFromCellConf( Cell cell, int maxWidth ) {
if ( maxWidth <= 0 ) {
return Optional.empty();
}
Optional<FlowCellSpanPolicy> autoSpan = cell.autoSpan();
if (!autoSpan.isPresent()) {
return Optional.empty();
}
int cellsToFill = autoSpan.get().cellsToFill();
int unusableSpace = ((cell.numberOfComponentsInRow()-1) * UI.scale(_horizontalGapSize));
int width = ((maxWidth - unusableSpace) * cellsToFill) / NUMBER_OF_COLUMNS;
// The height has to be requested for the width the component is about to
// receive, or a component which wraps its own content (like a nested grid)
// would report the number of rows it had during the previous layout pass.
Dimension newSize = new Dimension(width, _preferredHeightAtWidth(cell.component(), width));
return Optional.of(newSize);
}
private static int rankOf(ParentSizeClass sizeClass){
switch (sizeClass) {
case VOID:
return 0;
case VERY_SMALL:
return 1;
case SMALL:
return 2;
case MEDIUM:
return 3;
case LARGE:
return 4;
case VERY_LARGE:
return 5;
case OVERSIZE:
return 6;
default:
return -1;
}
}
private static Optional<FlowCellSpanPolicy> _findNextBestAutoSpan( FlowCellConf cell, ParentSizeClass targetSize ) {
Optional<FlowCellSpanPolicy> autoSpan = _find(rankOf(targetSize), cell);
if ( autoSpan.isPresent() )
return autoSpan;
// We did not find the exact match. Let's try to find the closest match.
int numberOfSizeClasses = ParentSizeClass.values().length;
int targetOrdinal = rankOf(targetSize);
/*
We want to find the enum value which is closed to the target ordinal.
*/
int sign = ( rankOf(targetSize) > numberOfSizeClasses / 2 ? 1 : -1 );
for ( int offset = 1; offset < numberOfSizeClasses; offset++ ) {
sign = -sign;
autoSpan = _find(targetOrdinal + offset * sign, cell);
if ( autoSpan.isPresent() )
return autoSpan;
sign = -sign;
autoSpan = _find(targetOrdinal + offset * sign, cell);
if ( autoSpan.isPresent() )
return autoSpan;
}
return Optional.empty();
}
private static Optional<FlowCellSpanPolicy> _find( int ordinal, FlowCellConf cell ) {
if ( ordinal < 0 || ordinal >= ParentSizeClass.values().length ) {
return Optional.empty();
}
ParentSizeClass targetSize = ParentSizeClass.values()[ordinal];
for ( FlowCellSpanPolicy autoSpan : cell.autoSpans() ) {
if ( autoSpan.parentSize() == targetSize ) {
return Optional.of(autoSpan);
}
}
return Optional.empty();
}
private static final class Cell {
private final Component component;
private final @Nullable FlowCellSpanPolicy autoSpan;
private final @Nullable FlowCellConf cellConf;
private AtomicInteger numberOfComponents;
Cell(
Component component,
AtomicInteger componentCounter,
@Nullable FlowCellSpanPolicy autoSpan,
@Nullable FlowCellConf cellConf
) {
this.component = component;
this.numberOfComponents = componentCounter;
this.autoSpan = autoSpan;
this.cellConf = cellConf;
}
public Component component() {
return component;
}
public Optional<FlowCellSpanPolicy> autoSpan() {
return Optional.ofNullable(autoSpan);
}
public Optional<FlowCellConf> flowCell() {
return Optional.ofNullable(cellConf);
}
public int numberOfComponentsInRow() {
return numberOfComponents.get();
}
public void setNumberOfComponents(AtomicInteger numberOfComponents) {
this.numberOfComponents = numberOfComponents;
}
}
}