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.UI;
- import javax.swing.JComponent;
- 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
- * {@code FlowLayout.LEFT}, {@code FlowLayout.RIGHT},
- * {@code FlowLayout.CENTER}, {@code FlowLayout.LEADING},
- * or {@code FlowLayout.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 {@code UI.HorizontalAlignment.LEFT},
- * {@code UI.HorizontalAlignment.RIGHT}, {@code UI.HorizontalAlignment.CENTER},
- * {@code UI.HorizontalAlignment.LEADING},
- * or {@code 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>{@code UI.HorizontalAlignment.LEFT}
- * <li>{@code UI.HorizontalAlignment.RIGHT}
- * <li>{@code UI.HorizontalAlignment.CENTER}
- * <li>{@code UI.HorizontalAlignment.LEADING}
- * <li>{@code 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 ) {
- synchronized (target.getTreeLock()) {
- 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);
- }
- Insets insets = target.getInsets();
- dim.width += insets.left + insets.right + hgap * 2;
- dim.height += insets.top + insets.bottom + vgap * 2;
- return dim;
- }
- }
- /**
- * 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, @Nullable int[] ascent,
- @Nullable int[] 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;
- }
- 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);
- final int generalMaxWidth = target.getPreferredSize().width - (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 = m.getPreferredSize();
- try {
- d = _dimensionsFromCellConf(cells[i], maxwidth).orElse(d);
- } catch (Exception e) {
- log.error("Error applying cell configuration", e);
- }
- 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
- */
- 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;
- Dimension newSize = new Dimension(width, cell.component().getPreferredSize().height);
- return Optional.of(newSize);
- }
- private static Optional<FlowCellSpanPolicy> _findNextBestAutoSpan( FlowCellConf cell, ParentSizeClass targetSize ) {
- Optional<FlowCellSpanPolicy> autoSpan = _find(targetSize.ordinal(), 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 = targetSize.ordinal();
- /*
- We want to find the enum value which is closed to the target ordinal.
- */
- int sign = ( targetSize.ordinal() > 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;
- }
- }
- }