FlowCellConf.java

package swingtree.layout;

import com.google.errorprone.annotations.Immutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import swingtree.UI;
import swingtree.UIForAnySwing;
import swingtree.api.Configurator;

import java.util.Arrays;
import java.util.Objects;

/**
 *  An immutable configuration object used to define how a {@link FlowCell} should
 *  place its associated component within a {@link ResponsiveGridFlowLayout}.<br>
 *  Instances of this are passed to the {@link swingtree.api.Configurator} of a {@link FlowCell}
 *  so that you can dynamically assign the number of cells a component should span,
 *  based on the size category of the parent container.<br>
 *  <br>
 *  Use {@link UI#AUTO_SPAN(Configurator)} to create a {@link FlowCell} from a {@link Configurator}
 *  and pass it to the {@link swingtree.UIForAnySwing#add(AddConstraint, UIForAnySwing[])}
 *  of a SwingTree UI declaration.<br>
 *  Here an example demonstrating how this might be used:
 *  <pre>{@code
 *    UI.panel().withPrefSize(400, 300)
 *    .withFlowLayout()
 *    .add(UI.AUTO_SPAN( it->it.small(12).medium(6).large(8) ),
 *         html("A red cell").withStyle(it->it
 *             .backgroundColor(UI.Color.RED)
 *         )
 *    )
 *    .add(UI.AUTO_SPAN( it->it.small(12).medium(6).large(4) ),
 *         html("a green cell").withStyle(it->it
 *             .backgroundColor(Color.GREEN)
 *         )
 *    )
 *  }</pre>
 *  In the above example, the {@code it} parameter of the {@code Configurator} is an instance of this class.
 *  The {@link Configurator} is called every time the {@link ResponsiveGridFlowLayout} updates the layout
 *  of the parent container, so that the number of cells a component spans can be adjusted dynamically.<br>
 *  The {@link UIForAnySwing#withFlowLayout()} creates a {@link ResponsiveGridFlowLayout} and attaches it to the panel.<br>
 *  <p><b>
 *      Note that the {@link FlowCell} configuration may only take effect if the parent
 *      container has a {@link ResponsiveGridFlowLayout} as a {@link java.awt.LayoutManager} installed.
 *  </b>
 *  <br><br>
 *  <p>
 *  Besides configuring the number of cells to span, you can also
 *  define how the cell should be filled vertically by the component
 *  and how the component should be aligned vertically within the cell.<br>
 *  Use the {@link #fill(boolean)} method to set the fill flag to {@code true}
 *  if you want the component to fill the cell vertically.<br>
 *  Use the {@link #align(UI.VerticalAlignment)} method to set the vertical alignment
 *  of the component within the cell to either {@link UI.VerticalAlignment#TOP},
 *  {@link UI.VerticalAlignment#CENTER}, or {@link UI.VerticalAlignment#BOTTOM}.
 */
@Immutable
public final class FlowCellConf
{
    private static final Logger log = LoggerFactory.getLogger(FlowCellConf.class);
    private final int                    _maxCellsToFill;
    private final Size                   _parentSize;
    private final ParentSizeClass        _parentSizeCategory;
    @SuppressWarnings("Immutable")
    private final FlowCellSpanPolicy[]   _autoSpans;
    private final boolean                _fill;
    private final UI.VerticalAlignment   _verticalAlignment;


    FlowCellConf(
            int                  maxCellsToFill,
            Size                 parentSize,
            ParentSizeClass      parentSizeCategory,
            FlowCellSpanPolicy[] autoSpans,
            boolean              fill,
            UI.VerticalAlignment alignVertically
    ) {
        _maxCellsToFill     = maxCellsToFill;
        _parentSize         = Objects.requireNonNull(parentSize);
        _parentSizeCategory = Objects.requireNonNull(parentSizeCategory);
        _autoSpans          = Objects.requireNonNull(autoSpans.clone());
        _fill               = fill;
        _verticalAlignment = alignVertically;
    }

    FlowCellSpanPolicy[] autoSpans() {
        return _autoSpans.clone();
    }

    /**
     *  Returns the maximum number of cells that a component can span
     *  in a grid layout managed by a {@link ResponsiveGridFlowLayout}.<br>
     *  The default value is 12, which is the maximum number of cells in a row
     *  of the {@link ResponsiveGridFlowLayout}.
     *  You may use this value to respond to it dynamically in the {@link Configurator}.
     *
     *  @return The maximum number of cells that a component can span in a grid layout.
     */
    public int maxCellsToFill() {
        return _maxCellsToFill;
    }

    /**
     *  Returns the {@link Size} object that represents the parent container's size.
     *  The cell exposes this information so that you can use for a more
     *  informed decision on how many cells a component should span.
     *
     *  @return The width and height of the parent container of the component
     *          associated with this cell span configuration.
     */
    public Size parentSize() {
        return _parentSize;
    }

    /**
     *  Returns the {@link ParentSizeClass} category that represents the parent container's width
     *  relative to its preferred width.<br>
     *  The cell exposes this information so that you can use for a more
     *  informed decision on how many cells a component should span.<br>
     *  <p>
     *  The {@link ParentSizeClass} category is calculated by dividing the parent container's width
     *  by the preferred width of the parent container. <br>
     *  So a parent container is considered large if its size is close to the preferred size,
     *  and {@link ParentSizeClass#OVERSIZE} if it is significantly larger.
     *
     *  @return The category of the parent container's size.
     */
    public ParentSizeClass parentSizeCategory() {
        return _parentSizeCategory;
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  for a given {@link ParentSizeClass} category.<br>
     *
     *  @param size The {@link ParentSizeClass} category to set.
     *  @param cellsToFill The number of cells to fill.
     *  @return A new {@link FlowCellConf} instance with the given {@link ParentSizeClass} and number of cells to fill.
     */
    public FlowCellConf with( ParentSizeClass size, int cellsToFill ) {
        Objects.requireNonNull(size);
        if ( cellsToFill < 0 ) {
            log.warn(
                    "Encountered negative number '"+cellsToFill+"' for cells " +
                    "to fill as part of the '"+ResponsiveGridFlowLayout.class.getSimpleName()+"' layout.",
                    new Throwable()
                );
            cellsToFill = 0;
        } else if ( cellsToFill > _maxCellsToFill ) {
            log.warn(
                    "Encountered number '"+cellsToFill+"' for cells to fill that is greater " +
                    "than the maximum number of cells to fill '"+_maxCellsToFill+"' " +
                    "as part of the '"+ResponsiveGridFlowLayout.class.getSimpleName()+"' layout.",
                    new Throwable()
                );
            cellsToFill = _maxCellsToFill;
        }
        FlowCellSpanPolicy[] autoSpans = new FlowCellSpanPolicy[_autoSpans.length+1];
        System.arraycopy(_autoSpans, 0, autoSpans, 0, _autoSpans.length);
        autoSpans[_autoSpans.length] = FlowCellSpanPolicy.of(size, cellsToFill);
        return new FlowCellConf(_maxCellsToFill, _parentSize, _parentSizeCategory, autoSpans, _fill, _verticalAlignment);
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  when the parent container is categorized as {@link ParentSizeClass#VERY_SMALL}.<br>
     *  A parent container is considered "very small" if its width
     *  is between 0/5 and 1/5 of its preferred width.<br>
     *  <p>
     *  The supplied number of cells to fill should be in the inclusive range of 0 to {@link #maxCellsToFill()}.<br>
     *  Values outside this range will be clamped to the nearest valid value and a warning will be logged.
     *
     *  @param span The number of cells to span as part of a grid
     *                     with a total of {@link #maxCellsToFill()} number of cells horizontally.
     *  @return A new {@link FlowCellConf} instance with the given number of cells to fill
     *          for the {@link ParentSizeClass#VERY_SMALL} category.
     */
    public FlowCellConf verySmall( int span ) {
        return with(ParentSizeClass.VERY_SMALL, span);
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  when the parent container is categorized as {@link ParentSizeClass#SMALL}.<br>
     *  A parent container is considered "small" if its width
     *  is between 1/5 and 2/5 of its preferred width.<br>
     *  <p>
     *  The supplied number of cells to fill should be in the inclusive range of 0 to {@link #maxCellsToFill()}.<br>
     *  Values outside this range will be clamped to the nearest valid value and a warning will be logged.
     *
     *  @param span The number of cells to span as part of a grid
     *                     with a total of {@link #maxCellsToFill()} number of cells horizontally.
     *  @return A new {@link FlowCellConf} instance with the given number of cells to fill
     *          for the {@link ParentSizeClass#SMALL} category.
     */
    public FlowCellConf small( int span ) {
        return with(ParentSizeClass.SMALL, span);
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  when the parent container is categorized as {@link ParentSizeClass#MEDIUM}.<br>
     *  A parent container is considered "medium" if its width
     *  is between 2/5 and 3/5 of its preferred width.<br>
     *  <p>
     *  The supplied number of cells to fill should be in the inclusive range of 0 to {@link #maxCellsToFill()}.<br>
     *  Values outside this range will be clamped to the nearest valid value and a warning will be logged.
     *
     *  @param span The number of cells to span as part of a grid
     *                     with a total of {@link #maxCellsToFill()} number of cells horizontally.
     *  @return A new {@link FlowCellConf} instance with the given number of cells to fill
     *          for the {@link ParentSizeClass#MEDIUM} category.
     */
    public FlowCellConf medium( int span ) {
        return with(ParentSizeClass.MEDIUM, span);
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  when the parent container is categorized as {@link ParentSizeClass#LARGE}.<br>
     *  A parent container is considered "large" if its width
     *  is between 3/5 and 4/5 of its preferred width.<br>
     *  <p>
     *  The supplied number of cells to fill should be in the inclusive range of 0 to {@link #maxCellsToFill()}.<br>
     *  Values outside this range will be clamped to the nearest valid value and a warning will be logged.
     *
     *  @param span The number of cells to span as part of a grid
     *                     with a total of {@link #maxCellsToFill()} number of cells horizontally.
     *  @return A new {@link FlowCellConf} instance with the given number of cells to fill
     *          for the {@link ParentSizeClass#LARGE} category.
     */
    public FlowCellConf large( int span ) {
        return with(ParentSizeClass.LARGE, span);
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  when the parent container is categorized as {@link ParentSizeClass#VERY_LARGE}.<br>
     *  A parent container is considered "very large" if its width
     *  is between 4/5 and 5/5 of its preferred width.<br>
     *  <p>
     *  The supplied number of cells to fill should be in the inclusive range of 0 to {@link #maxCellsToFill()}.<br>
     *  Values outside this range will be clamped to the nearest valid value and a warning will be logged.
     *
     *  @param span The number of cells to span as part of a grid
     *                     with a total of {@link #maxCellsToFill()} number of cells horizontally.
     *  @return A new {@link FlowCellConf} instance with the given number of cells to fill
     *          for the {@link ParentSizeClass#VERY_LARGE} category.
     */
    public FlowCellConf veryLarge( int span ) {
        return with(ParentSizeClass.VERY_LARGE, span);
    }

    /**
     *  Returns a new and updated {@link FlowCellConf} instance with an additional
     *  {@link FlowCellSpanPolicy} that specifies the number of cells to fill
     *  when the parent container is categorized as {@link ParentSizeClass#OVERSIZE}.<br>
     *  A parent container is considered "oversize" if its width is greater than its preferred width.<br>
     *  <p>
     *  The supplied number of cells to fill should be in the inclusive range of 0 to {@link #maxCellsToFill()}.<br>
     *  Values outside this range will be clamped to the nearest valid value and a warning will be logged.
     *
     *  @param span The number of cells to span as part of a grid
     *                     with a total of {@link #maxCellsToFill()} number of cells horizontally.
     *  @return A new {@link FlowCellConf} instance with the given number of cells to fill
     *          for the {@link ParentSizeClass#OVERSIZE} category.
     */
    public FlowCellConf oversize( int span ) {
        return with(ParentSizeClass.OVERSIZE, span);
    }

    boolean fill() {
        return _fill;
    }

    /**
     *  This flag defines how the grid cell in the flow layout should be filled
     *  by the component to which this cell configuration belongs.<br>
     *  The default behavior is to not fill the cell, but rather to align the component
     *  vertically in the center of the cell and use the component's preferred height.<br>
     *  If this flag is set to {@code true}, the component will fill the cell vertically
     *  and use the full height of the cell, which is also the full height of the row.<br>
     *  Note that this will ignore the component's preferred height!
     *
     * @param fill Whether the cell should be filled by the component.
     * @return A new {@link FlowCellConf} instance with the given fill flag.
     */
    public FlowCellConf fill(boolean fill) {
        return new FlowCellConf(_maxCellsToFill, _parentSize, _parentSizeCategory, _autoSpans, fill, _verticalAlignment);
    }

    UI.VerticalAlignment verticalAlignment() {
        return _verticalAlignment;
    }

    /**
     *  The {@link UI.VerticalAlignment} of a flow cell tells the
     *  {@link ResponsiveGridFlowLayout} how to place the component
     *  vertically within the cell.<br>
     *  So if you want the component to be aligned at the top,
     *  use {@link UI.VerticalAlignment#TOP}, if you want it to be
     *  centered, use {@link UI.VerticalAlignment#CENTER}, and if you
     *  want it to be aligned at the bottom, use {@link UI.VerticalAlignment#BOTTOM}.
     *
     * @param alignment The vertical alignment of the component within the cell.
     * @return A new {@link FlowCellConf} instance with the given vertical alignment policy.
     */
    public FlowCellConf align(UI.VerticalAlignment alignment) {
        return new FlowCellConf(_maxCellsToFill, _parentSize, _parentSizeCategory, _autoSpans, _fill, alignment);
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "[" +
                    "maxCellsToFill="     + _maxCellsToFill + ", " +
                    "parentSize="         + _parentSize + ", " +
                    "parentSizeCategory=" + _parentSizeCategory + ", " +
                    "autoSpans="          + _autoSpans.length + ", " +
                    "fill="               + _fill + ", " +
                    "alignVertically="    + _verticalAlignment +
                "]";
    }

    @Override
    public boolean equals(Object obj) {
        if ( obj == this ) {
            return true;
        }
        if ( obj == null || obj.getClass() != this.getClass() ) {
            return false;
        }
        FlowCellConf that = (FlowCellConf)obj;
        return this._maxCellsToFill == that._maxCellsToFill
            && this._parentSize.equals(that._parentSize)
            && this._parentSizeCategory == that._parentSizeCategory
            && this._autoSpans.length == that._autoSpans.length
            && Arrays.deepEquals(this._autoSpans, that._autoSpans)
            && this._fill == that._fill
            && this._verticalAlignment == that._verticalAlignment;
    }

    @Override
    public int hashCode() {
        return Objects.hash(_maxCellsToFill, _parentSize, _parentSizeCategory, Arrays.hashCode(_autoSpans), _fill, _verticalAlignment);
    }
}