Layout.java
package swingtree.api;
import com.google.errorprone.annotations.Immutable;
import net.miginfocom.swing.MigLayout;
import org.jspecify.annotations.Nullable;
import swingtree.UI;
import swingtree.layout.AddConstraint;
import swingtree.layout.FlowCell;
import swingtree.layout.FlowCellConf;
import swingtree.layout.LayoutConstraint;
import swingtree.layout.MigAddConstraint;
import swingtree.layout.ResponsiveGridFlowLayout;
import swingtree.style.ComponentExtension;
import swingtree.style.ComponentStyleDelegate;
import swingtree.style.StyleConf;
import sprouts.Association;
import sprouts.Pair;
import swingtree.layout.Bounds;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.util.Objects;
/**
* An abstract representation of an immutable layout configuration for a specific component,
* for which layout manager specific implementations can be instantiated through
* various factory methods like {@link Layout#border()}, {@link Layout#flow()}, {@link Layout#grid(int, int)}...
* and then supplied to the style API through {@link ComponentStyleDelegate#layout(Layout)}
* so that the layout can then be installed onto a component dynamically.
* <p>
* The various layout types hold necessary information
* and implementation logic required for installing the layout onto a component
* through the {@link #installFor(JComponent)} method,
* which will be used by the style engine of SwingTree
* every time the layout object state changes compared to the previous state
* effectively making the layout mechanics of a component fully dynamic.
* <p>
* You may implement this interface to create custom layout configurations
* for other kinds of {@link LayoutManager} implementations.
* <p>
* This interface also contains various implementations
* for supporting the most common types of {@link LayoutManager}s.
*
* @see swingtree.UIForAnySwing#withLayout(sprouts.Val) For a common practical usecase, see this method.
*/
@Immutable
public interface Layout
{
/**
* @return A hash code value for this layout.
*/
@Override int hashCode();
/**
* @param o The object to compare this layout to.
* @return {@code true} if the supplied object is a layout
* that is equal to this layout, {@code false} otherwise.
*/
@Override boolean equals( Object o );
/**
* Installs this layout for the supplied component.
*
* @param component The component to install this layout for.
*/
void installFor( JComponent component );
/**
* A factory method for creating a layout that does nothing
* (i.e. it does not install any layout for a component).
* This is a no-op layout that can be used to represent the lack of a specific layout
* being set for a component without having to set the layout to {@code null}.
*
* @return A layout that does nothing, i.e. it does not install any layout for a component.
*/
static Layout unspecific() { return Constants.UNSPECIFIC_LAYOUT_CONSTANT; }
/**
* Returns a {@link None} layout that removes any existing {@link LayoutManager}
* from a component (sets it to {@code null}), enabling manual positioning of
* child components via their {@link Component#setBounds(int, int, int, int)} method.
* <p>
* To also specify the initial bounds of child components declaratively,
* chain {@link None#withChildBounds(Bounds...)} on the returned instance, or
* use the {@link #none(Bounds...)} shorthand factory.
*
* @return A {@link None} layout that removes any existing layout manager from a component.
*/
static None none() { return (None) Constants.NONE_LAYOUT_CONSTANT; }
/**
* A convenience factory that creates a {@link None} layout pre-loaded with
* per-child {@link Bounds}. This is a shorthand for:
* <pre>{@code
* Layout.none().withChildBounds(childBounds)
* }</pre>
* When {@code installFor} is called, the layout manager is first removed from
* the component (enabling absolute positioning), and then each supplied
* {@link Bounds} entry is applied to the corresponding child by index via
* {@link Component#setBounds(int, int, int, int)}.
* Because the underlying storage is a sparse {@link Association}, you only
* need to supply bounds for the children you actually want to position;
* children without an entry are left untouched.
*
* @param childBounds The {@link Bounds} to apply to the component's children,
* in child-index order.
* @return A {@link None} layout that removes any layout manager and applies
* the given bounds to the corresponding children.
*/
static None none( Bounds... childBounds ) {
return ((None) Constants.NONE_LAYOUT_CONSTANT).withChildBounds(childBounds);
}
/**
* The preferred factory method for creating a {@link MigLayout}-based layout configuration
* from type-safe {@link LayoutConstraint} objects.
* <p>
* {@link LayoutConstraint} is a composable, type-safe wrapper around MigLayout constraint
* strings. The recommended way to use it is through the constants and factory methods
* available via {@code import static swingtree.UI.*}, which can then be combined
* with {@link LayoutConstraint#and(LayoutConstraint)}:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Layout.mig( FILL.and(WRAP(2)), "[shrink][grow]", "[]8[]" )
* }</pre>
* Using {@link LayoutConstraint} instead of raw strings catches typos at call-site and
* makes constraint composition explicit and refactor-friendly.
* See <a href="http://www.miglayout.com/whitepaper.html">the MigLayout whitepaper</a>
* for full constraint documentation.
* <p>
* The returned {@link ForMigLayout} instance supports fluent chaining to specify
* per-child component constraints via the various
* {@link ForMigLayout#withChildConstraints(MigAddConstraint...)} overloads.
*
* @param constr The general layout constraints for the {@link MigLayout}
* (e.g. {@code FILL.and(WRAP(2))}).
* @param colConstr The column constraints for the {@link MigLayout}
* (e.g. {@code LayoutConstraint.of("[shrink][grow]")}).
* @param rowConstr The row constraints for the {@link MigLayout}
* (e.g. {@code LayoutConstraint.of("[]8[]")}).
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig(
LayoutConstraint constr,
LayoutConstraint colConstr,
LayoutConstraint rowConstr
) {
return new ForMigLayout( constr, colConstr, rowConstr );
}
/**
* A factory method for creating a {@link MigLayout}-based layout configuration
* from type-safe {@link LayoutConstraint} objects, without column constraints.
* This is the preferred approach over the plain-{@link String} overloads.
* <p>
* See {@link #mig(LayoutConstraint, LayoutConstraint, LayoutConstraint)} for
* full details on the {@link LayoutConstraint} API and chaining child constraints.
*
* @param constr The general layout constraints for the {@link MigLayout}.
* @param rowConstr The row constraints for the {@link MigLayout}.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig(
LayoutConstraint constr,
LayoutConstraint rowConstr
) {
return new ForMigLayout( constr, LayoutConstraint.of(""), rowConstr );
}
/**
* A factory method for creating a {@link MigLayout}-based layout configuration
* from a single type-safe {@link LayoutConstraint}, with no column or row constraints.
* This is the preferred approach over the plain-{@link String} overload.
* <p>
* See {@link #mig(LayoutConstraint, LayoutConstraint, LayoutConstraint)} for
* full details on the {@link LayoutConstraint} API and chaining child constraints.
*
* @param constr The general layout constraints for the {@link MigLayout}.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig( LayoutConstraint constr ) {
return new ForMigLayout( constr, LayoutConstraint.of(""), LayoutConstraint.of("") );
}
/**
* A convenience factory method that creates a {@link MigLayout}-based layout configuration
* from a single type-safe {@link LayoutConstraint} together with per-child
* {@link MigAddConstraint}s for the component's direct children.
* <p>
* The {@code childConstraints} are mapped positionally: the first entry is applied to the
* first child, the second to the second child, and so on.
* Excess entries (more constraints than children) are silently ignored;
* children without a matching entry keep whatever constraint the parent
* {@link MigLayout} already has for them.
* <p>
* This is a shorthand for:
* <pre>{@code
* Layout.mig(constr).withChildConstraints(childConstraints)
* }</pre>
* See {@link #mig(LayoutConstraint, LayoutConstraint, LayoutConstraint)} for full details
* on the {@link LayoutConstraint} API.
*
* @param constr The general layout constraints for the {@link MigLayout}.
* @param childConstraints The {@link MigAddConstraint}s to apply to the component's children,
* in child-index order.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig( LayoutConstraint constr, MigAddConstraint... childConstraints ) {
return mig(constr).withChildConstraints(childConstraints);
}
/**
* A convenience overload of {@link #mig(LayoutConstraint, LayoutConstraint, LayoutConstraint)}
* that accepts plain constraint strings instead of {@link LayoutConstraint} objects.
* Each string is wrapped via {@link LayoutConstraint#of(String...)} before being forwarded.
* <p>
* Prefer the {@link LayoutConstraint}-based overloads for new code, as they are
* composable and less error-prone than raw strings.
* Click <a href="http://www.miglayout.com/">here</a> for more information about MigLayout.
*
* @param constr The general layout constraints string for the {@link MigLayout}.
* @param colConstr The column constraints string for the {@link MigLayout}.
* @param rowConstr The row constraints string for the {@link MigLayout}.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig(
String constr,
String colConstr,
String rowConstr
) {
return mig( LayoutConstraint.of(constr), LayoutConstraint.of(colConstr), LayoutConstraint.of(rowConstr) );
}
/**
* A convenience overload of {@link #mig(LayoutConstraint, LayoutConstraint)}
* that accepts plain constraint strings instead of {@link LayoutConstraint} objects.
* Each string is wrapped via {@link LayoutConstraint#of(String...)} before being forwarded.
* <p>
* Prefer the {@link LayoutConstraint}-based overloads for new code, as they are
* composable and less error-prone than raw strings.
* Click <a href="http://www.miglayout.com/">here</a> for more information about MigLayout.
*
* @param constr The general layout constraints string for the {@link MigLayout}.
* @param rowConstr The row constraints string for the {@link MigLayout}.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig(
String constr,
String rowConstr
) {
return mig( LayoutConstraint.of(constr), LayoutConstraint.of(rowConstr) );
}
/**
* A convenience overload of {@link #mig(LayoutConstraint)}
* that accepts a plain constraint string instead of a {@link LayoutConstraint} object.
* The string is wrapped via {@link LayoutConstraint#of(String...)} before being forwarded.
* <p>
* Prefer the {@link LayoutConstraint}-based overloads for new code, as they are
* composable and less error-prone than raw strings.
* In case you are not familiar with the MigLayout constraints, you can find more information
* about them <a href="http://www.miglayout.com/whitepaper.html">here</a>.
*
* @param constr The general layout constraints string for the {@link MigLayout}.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig( String constr ) {
return mig( LayoutConstraint.of(constr) );
}
/**
* A convenience factory method that creates a {@link MigLayout}-based layout configuration
* from a plain constraint string together with per-child {@link MigAddConstraint}s
* for the component's direct children.
* <p>
* The {@code childConstraints} are mapped positionally: the first entry is applied to the
* first child, the second to the second child, and so on.
* This is a shorthand for:
* <pre>{@code
* Layout.mig(constr).withChildConstraints(childConstraints)
* }</pre>
* Prefer the {@link LayoutConstraint}-based overload
* {@link #mig(LayoutConstraint, MigAddConstraint...)} for new code.
*
* @param constr The general layout constraints string for the {@link MigLayout}.
* @param childConstraints The {@link MigAddConstraint}s to apply to the component's children,
* in child-index order.
* @return A {@link ForMigLayout} configured with the supplied constraints.
*/
static ForMigLayout mig( String constr, MigAddConstraint... childConstraints ) {
return mig( LayoutConstraint.of(constr) ).withChildConstraints(childConstraints);
}
/**
* A factory method for creating a {@link ForFlowLayout} configuration that installs
* a {@link ResponsiveGridFlowLayout} onto a component with the given alignment and gaps.
* <p>
* The returned {@link ForFlowLayout} supports fluent chaining to specify per-child
* {@link FlowCell} constraints via the various
* {@link ForFlowLayout#withChildConstraints(FlowCell...)} overloads, enabling fully
* reactive responsive layouts when used together with
* {@link swingtree.UIForAnySwing#withLayout(sprouts.Val)}.
*
* @param align The alignment for the layout, which has to be one of <ul>
* <li>{@link UI.HorizontalAlignment#LEFT}</li>
* <li>{@link UI.HorizontalAlignment#CENTER}</li>
* <li>{@link UI.HorizontalAlignment#RIGHT}</li>
* <li>{@link UI.HorizontalAlignment#LEADING}</li>
* <li>{@link UI.HorizontalAlignment#TRAILING}</li>
* </ul>
* @param hgap The horizontal gap between components, in pixels.
* @param vgap The vertical gap between component rows, in pixels.
* @return A {@link ForFlowLayout} configured with the supplied alignment and gaps.
*/
static ForFlowLayout flow( UI.HorizontalAlignment align, int hgap, int vgap ) {
return new ForFlowLayout( align, hgap, vgap );
}
/**
* A factory method for creating a {@link ForFlowLayout} configuration that installs
* a {@link ResponsiveGridFlowLayout} with the given alignment and default gaps of 5 pixels.
* <p>
* The returned {@link ForFlowLayout} supports fluent chaining to specify per-child
* {@link FlowCell} constraints.
* See {@link #flow(UI.HorizontalAlignment, int, int)} for full details.
*
* @param align The horizontal alignment for the flow of components.
* @return A {@link ForFlowLayout} configured with the supplied alignment.
*/
static ForFlowLayout flow( UI.HorizontalAlignment align ) {
return new ForFlowLayout( align, 5, 5 );
}
/**
* A factory method for creating a {@link ForFlowLayout} configuration that installs
* a {@link ResponsiveGridFlowLayout} with centered alignment and default gaps of 5 pixels.
* <p>
* The returned {@link ForFlowLayout} supports fluent chaining to specify per-child
* {@link FlowCell} constraints.
* See {@link #flow(UI.HorizontalAlignment, int, int)} for full details.
*
* @return A {@link ForFlowLayout} with centered alignment and 5-pixel gaps.
*/
static ForFlowLayout flow() {
return new ForFlowLayout( UI.HorizontalAlignment.CENTER, 5, 5 );
}
/**
* A convenience factory method that creates a centered {@link ForFlowLayout} pre-loaded
* with per-child {@link FlowCell} constraints. This is a shorthand for:
* <pre>{@code
* Layout.flow().withChildConstraints(childConstraints)
* }</pre>
* Each {@link FlowCell} is typically created via {@link swingtree.UI#AUTO_SPAN(Configurator)}:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Var<Layout> layout = Var.of(
* Layout.flow(
* AUTO_SPAN( it -> it.small(12).medium(6) ),
* AUTO_SPAN( it -> it.small(12).medium(6) )
* )
* );
* UI.panel()
* .withLayout(layout)
* .add( label("Left") )
* .add( label("Right") );
* }</pre>
* Changing the {@code layout} property at runtime will reinstall the layout and re-push
* all child {@link FlowCell} constraints, making the span behaviour fully reactive.
*
* @param childConstraints The {@link FlowCell} constraints to apply to the component's
* children in child-index order.
* @return A {@link ForFlowLayout} with centered alignment and the given child constraints.
*/
static ForFlowLayout flow( FlowCell... childConstraints ) {
return flow().withChildConstraints(childConstraints);
}
/**
* A convenience factory method that creates a {@link ForFlowLayout} with the given
* alignment and per-child {@link FlowCell} constraints pre-loaded. This is a shorthand for:
* <pre>{@code
* Layout.flow(align).withChildConstraints(childConstraints)
* }</pre>
* See {@link #flow(FlowCell...)} for a usage example and reactive design notes.
*
* @param align The horizontal alignment for the flow of components.
* @param childConstraints The {@link FlowCell} constraints to apply to the component's
* children in child-index order.
* @return A {@link ForFlowLayout} with the given alignment and child constraints.
*/
static ForFlowLayout flow( UI.HorizontalAlignment align, FlowCell... childConstraints ) {
return flow(align).withChildConstraints(childConstraints);
}
/**
* A factory method for creating a layout that installs the {@link BorderLayout}
* onto a component based on the supplied parameters.
*
* @param horizontalGap The horizontal gap for the layout.
* @param verticalGap The vertical gap for the layout.
* @return A layout that installs the {@link BorderLayout} onto a component.
*/
static Layout border( int horizontalGap, int verticalGap ) {
return new BorderLayoutInstaller( horizontalGap, verticalGap );
}
/**
* A factory method for creating a layout that installs the {@link BorderLayout}
* onto a component based on the supplied parameters.
* The installed layout will have a default gap of 0 pixels.
*
* @return A layout that installs the {@link BorderLayout} onto a component.
*/
static Layout border() {
return new BorderLayoutInstaller( 0, 0 );
}
/**
* A factory method for creating a layout that installs the {@link GridLayout}
* onto a component based on the supplied parameters.
*
* @param rows The number of rows for the layout.
* @param cols The number of columns for the layout.
* @param horizontalGap The horizontal gap for the layout.
* @param verticalGap The vertical gap for the layout.
* @return A layout that installs the {@link GridLayout} onto a component.
*/
static Layout grid( int rows, int cols, int horizontalGap, int verticalGap ) {
return new GridLayoutInstaller( rows, cols, horizontalGap, verticalGap );
}
/**
* A factory method for creating a layout that installs the {@link GridLayout}
* onto a component based on the supplied parameters.
* The installed layout will have a default gap of 0 pixels.
*
* @param rows The number of rows for the layout.
* @param cols The number of columns for the layout.
* @return A layout that installs the {@link GridLayout} onto a component.
*/
static Layout grid( int rows, int cols ) {
return new GridLayoutInstaller( rows, cols, 0, 0 );
}
/**
* A factory method for creating a layout that installs the {@link BoxLayout}
* onto a component based on the supplied {@link UI.Axis} parameter.
* The axis determines whether the layout will be a horizontal or vertical
* {@link BoxLayout}.
*
* @param axis The axis for the layout, which has to be one of <ul>
* <li>{@link UI.Axis#X}</li>
* <li>{@link UI.Axis#Y}</li>
* <li>{@link UI.Axis#LINE}</li>
* <li>{@link UI.Axis#PAGE}</li>
* </ul>
*
* @return A layout that installs the {@link BoxLayout} onto a component.
*/
static Layout box( UI.Axis axis ) {
return new ForBoxLayout( axis.forBoxLayout() );
}
/**
* A factory method for creating a layout that installs the {@link BoxLayout}
* onto a component with a default axis of {@link UI.Axis#X}.
*
* @return A layout that installs the default {@link BoxLayout} onto a component.
*/
static Layout box() {
return new ForBoxLayout( BoxLayout.X_AXIS );
}
/**
* The {@link Unspecific} layout is a layout that represents the lack
* of a specific layout being set for a component.
* Note that this does not represent the absence of a {@link LayoutManager}
* for a component, but rather the absence of it being specified.
* This means that whatever layout is currently installed for a component
* will be left as is, and no other layout will be installed for the component.
* <p>
* Note that this is different from the {@link None} layout,
* which represents the absence of a {@link LayoutManager}
* for a component (i.e. it removes any existing layout from the component and sets it to {@code null}).
*/
@Immutable
final class Unspecific implements Layout
{
Unspecific() {}
@Override public int hashCode() { return 0; }
@Override
public boolean equals( Object o ) {
if ( o == null ) return false;
if ( o == this ) return true;
return o.getClass() == getClass();
}
@Override public String toString() { return getClass().getSimpleName() + "[]"; }
/**
* Does nothing.
* @param component The component to install the layout for.
*/
@Override public void installFor( JComponent component ) { /* Do nothing. */ }
}
/**
* The {@link None} layout removes any existing {@link LayoutManager} from a component
* (sets it to {@code null}), enabling fully manual positioning of child components
* via {@link Component#setBounds(int, int, int, int)}.
* <p>
* Beyond simply clearing the layout manager, a {@link None} instance can carry a sparse
* {@link Association} of per-child {@link Bounds} that are applied to the component's
* direct children during {@link #installFor(JComponent)}. This lets you declare the
* absolute position and size of each child you care about right inside the
* {@link Layout} object, keeping the layout specification co-located with the
* component tree rather than scattered across imperative setup code:
* <pre>{@code
* import static swingtree.UI.*;
* import swingtree.layout.Bounds;
* // ...
* Var<Layout> layout = Var.of(
* Layout.none(
* Bounds.of( 0, 0, 120, 40), // child 0
* Bounds.of(130, 0, 120, 40), // child 1
* Bounds.of( 0, 50, 250, 80) // child 2
* )
* );
*
* UI.panel().withLayout(layout)
* .add( button("A") )
* .add( button("B") )
* .add( label("C") );
* }</pre>
* Because the association is sparse you can also target a single child by index
* without touching the others:
* <pre>{@code
* layout.set( Layout.none().withChildBound(2, Bounds.of(0, 50, 250, 80)) );
* }</pre>
* <p>
* Note that this is different from the {@link Unspecific} layout, which does nothing
* at all — {@link None} actively removes the layout manager.
*/
@Immutable
@SuppressWarnings("Immutable")
final class None implements Layout
{
private final Association<Integer, Bounds> _childBounds;
None() {
this( Association.betweenSorted(Integer.class, Bounds.class) );
}
None( Association<Integer, Bounds> childBounds ) {
_childBounds = Objects.requireNonNull(childBounds);
}
// -- Per-child bounds withers --
/**
* Returns a new {@link None} layout whose per-child {@link Bounds} are replaced
* by the supplied sorted {@link Association}.
* <p>
* Keys are child indices ({@code 0} = first child, {@code 1} = second, etc.);
* the association is sparse, so you only need to include entries for the children
* you actually want to position. Children whose index has no entry are left
* untouched. An empty association produces the plain "remove layout only" behaviour.
*
* @param childBounds A sorted {@link Association} mapping child indices to
* the {@link Bounds} to apply.
* @return A new {@link None} layout with the updated child bounds,
* or the shared {@link Layout#none()} constant when the association is empty.
*/
public None withChildBounds( Association<Integer, Bounds> childBounds ) {
Objects.requireNonNull(childBounds);
if ( childBounds.isEmpty() )
return (None) Constants.NONE_LAYOUT_CONSTANT;
return new None(childBounds);
}
/**
* Returns a new {@link None} layout with per-child {@link Bounds} built from
* the supplied varargs array. Entries are mapped positionally: index 0
* applies to the first child, index 1 to the second, and so on.
* Passing an empty array returns the shared {@link Layout#none()} constant.
*
* @param childBounds The {@link Bounds} to apply to the component's children,
* in child-index order.
* @return A new {@link None} layout with the updated child bounds.
*/
public None withChildBounds( Bounds... childBounds ) {
Association<Integer, Bounds> assoc = Association.betweenSorted(Integer.class, Bounds.class);
for ( int i = 0; i < childBounds.length; i++ )
assoc = assoc.put(i, childBounds[i]);
return withChildBounds(assoc);
}
/**
* Returns a new {@link None} layout with the {@link Bounds} at the given child
* index replaced by the supplied value. All other child bounds are copied unchanged.
* <p>
* Because the underlying storage is a sparse {@link Association}, no padding is
* needed: the bound is stored at exactly {@code index}, regardless of whether
* lower indices have entries.
*
* @param index The zero-based index of the child whose bounds to update.
* @param childBound The new {@link Bounds} for the child at {@code index}.
* @return A new {@link None} layout with the updated child bound at {@code index}.
* @throws IndexOutOfBoundsException if {@code index} is negative.
*/
public None withChildBound( int index, Bounds childBound ) {
Objects.requireNonNull(childBound);
if ( index < 0 )
throw new IndexOutOfBoundsException("Child index must not be negative, but was: " + index);
return withChildBounds( _childBounds.put(index, childBound) );
}
/**
* Returns a new {@link None} layout with the supplied {@link Bounds} appended
* as the constraint for the next child in sequence (i.e. at index = max existing
* key + 1, or 0 if no bounds have been set yet).
*
* @param childBound The {@link Bounds} to append for the next child.
* @return A new {@link None} layout with the bound appended.
*/
public None withAddedChildBound( Bounds childBound ) {
Objects.requireNonNull(childBound);
int nextIndex = 0;
for ( Integer key : _childBounds.keySet() )
nextIndex = key + 1;
return withChildBounds( _childBounds.put(nextIndex, childBound) );
}
// -- Object contract --
@Override public int hashCode() { return _childBounds.hashCode(); }
@Override
public boolean equals( Object o ) {
if ( o == null ) return false;
if ( o == this ) return true;
if ( o.getClass() != getClass() ) return false;
None other = (None) o;
return _childBounds.equals(other._childBounds);
}
@Override public String toString() {
return getClass().getSimpleName() + "[childBounds=" + _childBounds + "]";
}
// -- Layout installation --
/**
* Installs this layout for the supplied component in two phases:
* <ol>
* <li><b>Layout removal</b> — the existing {@link LayoutManager} (if any) is
* replaced with {@code null}, enabling absolute positioning of child
* components.</li>
* <li><b>Child bounds</b> — if any per-child {@link Bounds} were specified,
* each stored entry is applied to the corresponding direct child of
* {@code component} (by index) via
* {@link Component#setBounds(int, int, int, int)}.
* Only entries that differ from the child's current bounds are written,
* and {@link JComponent#repaint()} is called exactly once at the end if
* anything changed.</li>
* </ol>
*
* @param component The component to remove the layout manager from and
* optionally apply child bounds to.
*/
@Override
public void installFor( JComponent component ) {
// Phase 1: remove the layout manager:
LayoutManager oldManager = component.getLayout();
if ( oldManager != null ) {
component.setLayout(null);
component.revalidate();
}
// Phase 2: apply per-child bounds if specified:
if ( _childBounds.isNotEmpty() ) {
Component[] children = component.getComponents();
boolean changed = false;
for ( Pair<Integer, Bounds> entry : _childBounds ) {
int i = entry.first();
if ( i < 0 || i >= children.length ) continue;
java.awt.Rectangle desired = UI.scale(entry.second().toRectangle());
java.awt.Rectangle existing = children[i].getBounds();
if ( !desired.equals(existing) ) {
children[i].setBounds(desired);
changed = true;
}
}
if ( changed )
component.repaint();
}
}
}
/**
* An immutable {@link Layout} implementation that configures and installs a
* {@link MigLayout} onto a component. It holds three kinds of constraints:
* <ul>
* <li><b>General layout constraints</b> ({@code constr}) — control global layout
* behaviour such as wrapping, filling, gaps and hiding mode.</li>
* <li><b>Column constraints</b> ({@code colConstr}) — define the sizing and
* alignment rules for each column in the grid.</li>
* <li><b>Row constraints</b> ({@code rowConstr}) — define the sizing and
* alignment rules for each row in the grid.</li>
* <li><b>Per-child component constraints</b> ({@code childConstraints}) — a
* sorted {@link Association} mapping child indices ({@link Integer}) to
* {@link MigAddConstraint}s applied to the component's direct children.
* Unlike a positional tuple, the association is sparse: you only need to
* include entries for the children you actually want to configure; children
* whose index has no entry keep whatever constraint the
* {@link MigLayout} already has for them.</li>
* </ul>
* <p>
* Instances are created via the {@link Layout#mig(LayoutConstraint)} family of
* factory methods and are further configured through the fluent {@code with*} wither
* methods, which all return a new immutable instance:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Layout.mig( FILL.and(WRAP(2)), "[shrink][grow]", "[]8[]" )
* .withChildConstraints( RIGHT, GROW_X, RIGHT, GROW_X )
* }</pre>
* Whenever any property of this configuration changes (detected by the style engine
* via {@link #equals}/{@link #hashCode}), the {@link #installFor(JComponent)} method
* is called, which surgically updates only the properties that have changed and
* calls {@link JComponent#revalidate()} to trigger a layout refresh.
*/
@Immutable
@SuppressWarnings("Immutable")
final class ForMigLayout implements Layout
{
private final LayoutConstraint _constr;
private final LayoutConstraint _colConstr;
private final LayoutConstraint _rowConstr;
private final Association<Integer, MigAddConstraint> _childConstraints;
ForMigLayout( LayoutConstraint constr, LayoutConstraint colConstr, LayoutConstraint rowConstr ) {
this( constr, colConstr, rowConstr, Association.betweenSorted(Integer.class, MigAddConstraint.class) );
}
ForMigLayout(
LayoutConstraint constr,
LayoutConstraint colConstr,
LayoutConstraint rowConstr,
Association<Integer, MigAddConstraint> childConstraints
) {
_constr = Objects.requireNonNull(constr);
_colConstr = Objects.requireNonNull(colConstr);
_rowConstr = Objects.requireNonNull(rowConstr);
_childConstraints = Objects.requireNonNull(childConstraints);
}
// -- General layout constraint withers --
/**
* Returns a new {@link ForMigLayout} instance with the supplied general layout constraints
* and all other properties copied from this instance.
* This is the preferred overload as it works with the type-safe {@link LayoutConstraint}
* API, which supports composition via {@link LayoutConstraint#and(LayoutConstraint)}.
*
* @param constr The new general layout constraints for the {@link MigLayout}.
* @return A new {@link ForMigLayout} instance with the updated layout constraints.
*/
public ForMigLayout withConstraint( LayoutConstraint constr ) {
return new ForMigLayout( constr, _colConstr, _rowConstr, _childConstraints );
}
/**
* Returns a new {@link ForMigLayout} instance with the supplied general layout constraints
* and all other properties copied from this instance.
* The string is wrapped via {@link LayoutConstraint#of(String...)} and forwarded
* to {@link #withConstraint(LayoutConstraint)}.
* Prefer {@link #withConstraint(LayoutConstraint)} for new code.
*
* @param constr The new general layout constraints string for the {@link MigLayout}.
* @return A new {@link ForMigLayout} instance with the updated layout constraints.
*/
public ForMigLayout withConstraint( String constr ) { return withConstraint( LayoutConstraint.of(constr) ); }
// -- Row constraint withers --
/**
* Returns a new {@link ForMigLayout} instance with the supplied row constraints
* and all other properties copied from this instance.
* This is the preferred overload as it works with the type-safe {@link LayoutConstraint}
* API, which supports composition via {@link LayoutConstraint#and(LayoutConstraint)}.
*
* @param rowConstr The new row constraints for the {@link MigLayout}.
* @return A new {@link ForMigLayout} instance with the updated row constraints.
*/
public ForMigLayout withRowConstraint( LayoutConstraint rowConstr ) {
return new ForMigLayout( _constr, _colConstr, rowConstr, _childConstraints );
}
/**
* Returns a new {@link ForMigLayout} instance with the supplied row constraints
* and all other properties copied from this instance.
* The string is wrapped via {@link LayoutConstraint#of(String...)} and forwarded
* to {@link #withRowConstraint(LayoutConstraint)}.
* Prefer {@link #withRowConstraint(LayoutConstraint)} for new code.
*
* @param rowConstr The new row constraints string for the {@link MigLayout}.
* @return A new {@link ForMigLayout} instance with the updated row constraints.
*/
public ForMigLayout withRowConstraint( String rowConstr ) { return withRowConstraint( LayoutConstraint.of(rowConstr) ); }
// -- Column constraint withers --
/**
* Returns a new {@link ForMigLayout} instance with the supplied column constraints
* and all other properties copied from this instance.
* This is the preferred overload as it works with the type-safe {@link LayoutConstraint}
* API, which supports composition via {@link LayoutConstraint#and(LayoutConstraint)}.
*
* @param colConstr The new column constraints for the {@link MigLayout}.
* @return A new {@link ForMigLayout} instance with the updated column constraints.
*/
public ForMigLayout withColumnConstraint( LayoutConstraint colConstr ) {
return new ForMigLayout( _constr, colConstr, _rowConstr, _childConstraints );
}
/**
* Returns a new {@link ForMigLayout} instance with the supplied column constraints
* and all other properties copied from this instance.
* The string is wrapped via {@link LayoutConstraint#of(String...)} and forwarded
* to {@link #withColumnConstraint(LayoutConstraint)}.
* Prefer {@link #withColumnConstraint(LayoutConstraint)} for new code.
*
* @param colConstr The new column constraints string for the {@link MigLayout}.
* @return A new {@link ForMigLayout} instance with the updated column constraints.
*/
public ForMigLayout withColumnConstraint( String colConstr ) { return withColumnConstraint( LayoutConstraint.of(colConstr) ); }
// -- Per-child component constraint withers --
/**
* Returns a new {@link ForMigLayout} instance whose per-child component constraints
* are replaced by the supplied sorted {@link Association}.
* <p>
* Keys are child indices ({@code 0} = first child, {@code 1} = second, etc.);
* the association is sparse, so you only need to include entries for children
* you actually want to configure.
* Children whose index has no entry in the association are left untouched.
* An empty association means no constraints are stored in this layout object;
* any constraints previously applied to children by an earlier {@code installFor}
* call remain in the {@link MigLayout} until explicitly overwritten.
*
* @param childConstraints A sorted {@link Association} mapping child indices to
* the {@link MigAddConstraint} to apply.
* @return A new {@link ForMigLayout} with the updated child constraints.
*/
public ForMigLayout withChildConstraints( Association<Integer, MigAddConstraint> childConstraints ) {
return new ForMigLayout( _constr, _colConstr, _rowConstr, Objects.requireNonNull(childConstraints) );
}
/**
* Returns a new {@link ForMigLayout} instance whose per-child component constraints
* are replaced by the supplied varargs array of {@link MigAddConstraint}s.
* <p>
* The constraints are mapped positionally to the component's children:
* the first argument applies to the first child, the second to the second, and so on.
* Children at indices beyond the supplied array length are left untouched.
* Passing an empty array stores no constraints in this layout object;
* any constraints previously applied to children by an earlier {@code installFor}
* call remain in the {@link MigLayout} until explicitly overwritten.
* <p>
* This is the most concise way to specify per-child constraints for common cases:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Layout.mig( FILL.and(WRAP(2)) )
* .withChildConstraints( RIGHT, GROW_X, RIGHT, GROW_X )
* }</pre>
*
* @param childConstraints The {@link MigAddConstraint}s to apply to the component's
* children, in child-index order.
* @return A new {@link ForMigLayout} with the updated child constraints.
*/
public ForMigLayout withChildConstraints( MigAddConstraint... childConstraints ) {
Association<Integer, MigAddConstraint> assoc = Association.betweenSorted(Integer.class, MigAddConstraint.class);
for ( int i = 0; i < childConstraints.length; i++ )
assoc = assoc.put(i, childConstraints[i]);
return withChildConstraints(assoc);
}
/**
* Returns a new {@link ForMigLayout} instance with the {@link MigAddConstraint} at the
* given child index replaced by the supplied value.
* All other child constraints and all other properties are copied unchanged.
* <p>
* Because the underlying storage is a sparse {@link Association}, no padding
* is needed: the constraint is stored at exactly {@code index}, regardless of
* whether lower indices have entries.
*
* @param index The zero-based index of the child whose constraint to update.
* The first child has index 0.
* @param childConstraint The new {@link MigAddConstraint} for the child at {@code index}.
* @return A new {@link ForMigLayout} with the updated child constraint at {@code index}.
* @throws IndexOutOfBoundsException if {@code index} is negative.
*/
public ForMigLayout withChildConstraint( int index, MigAddConstraint childConstraint ) {
Objects.requireNonNull(childConstraint);
if ( index < 0 )
throw new IndexOutOfBoundsException("Child index must not be negative, but was: " + index);
return withChildConstraints( _childConstraints.put(index, childConstraint) );
}
/**
* Returns a new {@link ForMigLayout} instance with the {@link MigAddConstraint} at the
* given child index replaced by a constraint wrapping the supplied string.
* The string is converted via {@link MigAddConstraint#of(String...)} and then forwarded
* to {@link #withChildConstraint(int, MigAddConstraint)}.
* <p>
* Because the underlying storage is a sparse {@link Association}, no padding is
* needed: the constraint is stored at exactly {@code index}, regardless of whether
* lower indices have entries.
*
* @param index The zero-based index of the child whose constraint to update.
* @param childConstraint The MigLayout component-constraint string for the child.
* @return A new {@link ForMigLayout} with the updated child constraint at {@code index}.
* @throws IndexOutOfBoundsException if {@code index} is negative.
*/
public ForMigLayout withChildConstraint( int index, String childConstraint ) {
return withChildConstraint( index, MigAddConstraint.of(childConstraint) );
}
/**
* Returns a new {@link ForMigLayout} instance with the supplied {@link MigAddConstraint}
* appended to the end of the existing child-constraint tuple.
* This is a convenient alternative to {@link #withChildConstraints(MigAddConstraint...)}
* when building up constraints one at a time:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Layout.mig( FILL.and(WRAP(2)) )
* .withAddedChildConstraint( RIGHT )
* .withAddedChildConstraint( GROW_X )
* .withAddedChildConstraint( RIGHT )
* .withAddedChildConstraint( GROW_X )
* }</pre>
*
* @param childConstraint The {@link MigAddConstraint} to append as the next child
* component constraint.
* @return A new {@link ForMigLayout} with the constraint appended.
*/
public ForMigLayout withAddedChildConstraint( MigAddConstraint childConstraint ) {
Objects.requireNonNull(childConstraint);
int nextIndex = 0;
for ( Integer key : _childConstraints.keySet() )
nextIndex = key + 1;
return withChildConstraints( _childConstraints.put(nextIndex, childConstraint) );
}
/**
* Returns a new {@link ForMigLayout} instance with a {@link MigAddConstraint} wrapping
* the supplied string appended to the end of the existing child-constraint tuple.
* The string is converted via {@link MigAddConstraint#of(String...)} and then forwarded
* to {@link #withAddedChildConstraint(MigAddConstraint)}.
*
* @param childConstraint The MigLayout component-constraint string to append.
* @return A new {@link ForMigLayout} with the constraint appended.
*/
public ForMigLayout withAddedChildConstraint( String childConstraint ) {
return withAddedChildConstraint( MigAddConstraint.of(childConstraint) );
}
// -- Object contract --
@Override public int hashCode() { return Objects.hash(_constr, _rowConstr, _colConstr, _childConstraints); }
@Override
public boolean equals( Object o ) {
if ( o == null ) return false;
if ( o == this ) return true;
if ( o.getClass() != getClass() ) return false;
ForMigLayout other = (ForMigLayout) o;
return _constr.equals(other._constr) &&
_rowConstr.equals(other._rowConstr) &&
_colConstr.equals(other._colConstr) &&
_childConstraints.equals(other._childConstraints);
}
// -- Layout installation --
/**
* Installs a {@link MigLayout} onto the supplied component and applies all constraints
* stored in this configuration.
* <p>
* The installation proceeds in three phases:
* <ol>
* <li><b>Self constraint</b> — if this component's own style holds a
* {@link StyleConf#layoutConstraint() layout constraint} and its parent uses a
* {@link MigLayout}, that constraint is pushed into the parent layout so the
* component is correctly positioned within the parent grid.</li>
* <li><b>Layout manager</b> — if none of the three constraint strings is empty, a
* {@link MigLayout} is installed (or updated in-place if one is already present)
* using the stored general, column, and row constraints.</li>
* <li><b>Child constraints</b> — if the child-constraint tuple is non-empty, each
* stored {@link MigAddConstraint} is applied to the corresponding direct child of
* {@code component} (by position). Only entries that differ from what the
* {@link MigLayout} already has are written, and
* {@link JComponent#revalidate()} is called exactly once at the end if anything
* changed.</li>
* </ol>
*
* @param component The component to install the {@link MigLayout} for.
*/
@Override
public void installFor( JComponent component ) {
ComponentExtension<?> extension = ComponentExtension.from(component);
StyleConf styleConf = extension.getStyle();
if ( styleConf.layoutConstraint().isPresent() ) {
// Phase 1: push this component's own layout constraint into its parent MigLayout:
LayoutManager parentLayout = ( component.getParent() == null ? null : component.getParent().getLayout() );
if ( parentLayout instanceof MigLayout) {
MigLayout migLayout = (MigLayout) parentLayout;
Object componentConstraints = styleConf.layoutConstraint().get();
Object currentComponentConstraints = migLayout.getComponentConstraints(component);
// ^ can be a String or a CC object, we compare it to the desired constraint:
if ( !componentConstraints.equals(currentComponentConstraints) ) {
migLayout.setComponentConstraints(component, componentConstraints);
component.getParent().revalidate();
}
}
}
// Phase 2: install / update the MigLayout on the component itself:
final String layoutConstraints = _constr.toString();
final String columnConstraints = _colConstr.toString();
final String rowConstraints = _rowConstr.toString();
if ( !layoutConstraints.isEmpty() || !columnConstraints.isEmpty() || !rowConstraints.isEmpty() ) {
LayoutManager currentLayout = component.getLayout();
if ( !( currentLayout instanceof MigLayout ) ) {
component.setLayout(new MigLayout( layoutConstraints, columnConstraints, rowConstraints ));
component.revalidate();
} else {
MigLayout migLayout = (MigLayout) currentLayout;
boolean layoutConstraintsChanged = !layoutConstraints.equals(migLayout.getLayoutConstraints());
boolean columnConstraintsChanged = !columnConstraints.equals(migLayout.getColumnConstraints());
boolean rowConstraintsChanged = !rowConstraints.equals(migLayout.getRowConstraints());
if ( layoutConstraintsChanged || columnConstraintsChanged || rowConstraintsChanged ) {
migLayout.setLayoutConstraints(layoutConstraints);
migLayout.setColumnConstraints(columnConstraints);
migLayout.setRowConstraints(rowConstraints);
component.revalidate();
}
}
}
// Phase 3: apply per-child component constraints:
if ( _childConstraints.isNotEmpty() ) {
LayoutManager currentLayout = component.getLayout();
if ( currentLayout instanceof MigLayout ) {
MigLayout migLayout = (MigLayout) currentLayout;
Component[] children = component.getComponents();
boolean childrenChanged = false;
for ( Pair<Integer, MigAddConstraint> entry : _childConstraints ) {
int i = entry.first();
if ( i < 0 || i >= children.length ) continue;
Object desired = entry.second().toConstraintForLayoutManager();
Object existing = migLayout.getComponentConstraints(children[i]);
if ( !desired.equals(existing) ) {
migLayout.setComponentConstraints(children[i], desired);
childrenChanged = true;
}
}
if ( childrenChanged )
component.revalidate();
}
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"constr=" + _constr + ", " +
"colConstr=" + _colConstr + ", " +
"rowConstr=" + _rowConstr + ", " +
"childConstraints=" + _childConstraints +
"]";
}
}
/**
* An immutable {@link Layout} implementation that configures and installs a
* {@link ResponsiveGridFlowLayout} onto a component. It holds:
* <ul>
* <li><b>Alignment</b> ({@code align}) — the horizontal alignment of components
* within each row of the flow (left, center, right, leading, or trailing).</li>
* <li><b>Horizontal gap</b> ({@code hgap}) — the pixel spacing between components
* in the same row.</li>
* <li><b>Vertical gap</b> ({@code vgap}) — the pixel spacing between rows.</li>
* <li><b>Per-child {@link FlowCell} constraints</b> ({@code childConstraints}) — a
* sorted {@link Association} mapping child indices ({@link Integer}) to
* {@link FlowCell}s that are pushed onto the component's direct children.
* Unlike a positional tuple, the association is sparse: you only need to include
* entries for the children you actually want to configure.
* Each {@link FlowCell} carries a responsive span policy (see
* {@link swingtree.UI#AUTO_SPAN(Configurator)}) that the
* {@link ResponsiveGridFlowLayout} queries on every layout pass to determine
* how many grid columns a child should occupy for the current parent size.</li>
* </ul>
* <p>
* The child-constraint tuple is the key to building <em>fully reactive</em> responsive
* layouts. With the static {@code UI.AUTO_SPAN()} approach every child's span policy
* is fixed at the time the component is added. When child constraints need to change
* at runtime (e.g. the number of columns depends on application state), wrap a
* {@link ForFlowLayout} in a {@link sprouts.Var} and pass it to
* {@link swingtree.UIForAnySwing#withLayout(sprouts.Val)}:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Var<Layout> layout = Var.of(
* Layout.flow( AUTO_SPAN(it -> it.small(12).medium(6)),
* AUTO_SPAN(it -> it.small(12).medium(6)) )
* );
*
* UI.panel()
* .withLayout(layout)
* .add( label("A") )
* .add( label("B") );
*
* // Later: swap to a single full-width column for both children:
* layout.set( Layout.flow( AUTO_SPAN(12), AUTO_SPAN(12) ) );
* }</pre>
* Changing the {@code layout} property triggers a style re-evaluation, which calls
* {@link #installFor(JComponent)}, which re-pushes the updated {@link FlowCell}s as
* client properties onto the children so that the next layout pass picks them up.
* <p>
* Instances are created via the {@link Layout#flow()} family of factory methods and
* are further configured through the fluent {@code with*} wither methods.
*/
@Immutable
@SuppressWarnings("Immutable")
final class ForFlowLayout implements Layout
{
private final UI.HorizontalAlignment _align;
private final int _horizontalGapSize;
private final int _verticalGapSize;
private final Association<Integer, FlowCell> _childConstraints;
ForFlowLayout( UI.HorizontalAlignment align, int hgap, int vgap ) {
this( align, hgap, vgap, Association.betweenSorted(Integer.class, FlowCell.class) );
}
ForFlowLayout(
UI.HorizontalAlignment align,
int hgap,
int vgap,
Association<Integer, FlowCell> childConstraints
) {
_align = Objects.requireNonNull(align);
_horizontalGapSize = hgap;
_verticalGapSize = vgap;
_childConstraints = Objects.requireNonNull(childConstraints);
}
// -- Alignment / gap withers --
/**
* Returns a new {@link ForFlowLayout} with the given horizontal alignment and
* all other properties copied unchanged.
*
* @param align The new horizontal alignment for the flow.
* @return A new {@link ForFlowLayout} with the updated alignment.
*/
public ForFlowLayout withAlignment( UI.HorizontalAlignment align ) {
return new ForFlowLayout( align, _horizontalGapSize, _verticalGapSize, _childConstraints );
}
/**
* Returns a new {@link ForFlowLayout} with the given horizontal gap size and
* all other properties copied unchanged.
*
* @param hgap The new horizontal gap between components, in pixels.
* @return A new {@link ForFlowLayout} with the updated horizontal gap.
*/
public ForFlowLayout withHorizontalGap( int hgap ) {
return new ForFlowLayout( _align, hgap, _verticalGapSize, _childConstraints );
}
/**
* Returns a new {@link ForFlowLayout} with the given vertical gap size and
* all other properties copied unchanged.
*
* @param vgap The new vertical gap between component rows, in pixels.
* @return A new {@link ForFlowLayout} with the updated vertical gap.
*/
public ForFlowLayout withVerticalGap( int vgap ) {
return new ForFlowLayout( _align, _horizontalGapSize, vgap, _childConstraints );
}
// -- Per-child FlowCell constraint withers --
/**
* Returns a new {@link ForFlowLayout} whose per-child {@link FlowCell} constraints
* are replaced by the supplied sorted {@link Association}.
* <p>
* Keys are child indices ({@code 0} = first child, {@code 1} = second, etc.);
* the association is sparse, so you only need to include entries for children
* you actually want to configure.
* Children whose index has no entry keep whatever {@link FlowCell} they already have.
* An empty association means no constraints are stored in this layout object;
* any {@link AddConstraint} client properties previously pushed to children
* by an earlier {@code installFor} call remain on those children until explicitly
* overwritten.<br>
* The intended way of creating {@link FlowCell}s is by using {@link UI#AUTO_SPAN(Configurator)}!<br>
* An important edge case to consider when writing a responsive flow layout:<br>
* <b>
* If a {@link FlowCell} is passed to the responsive flow layout without
* any span policies defined, it will always default to spanning 12 cells at all parent size categories!
* </b>
*
* @param childConstraints The positional {@link FlowCell} constraints for the children.
* @return A new {@link ForFlowLayout} with the updated child constraints.
*/
public ForFlowLayout withChildConstraints( Association<Integer, FlowCell> childConstraints ) {
return new ForFlowLayout( _align, _horizontalGapSize, _verticalGapSize,
Objects.requireNonNull(childConstraints) );
}
/**
* Returns a new {@link ForFlowLayout} whose per-child {@link FlowCell} constraints
* are replaced by the supplied varargs array.
* <p>
* The constraints are mapped positionally to the component's children:
* the first argument applies to the first child, the second to the second, and so on.
* Passing an empty array stores no constraints in this layout object;
* any {@link AddConstraint} client properties previously pushed to children
* by an earlier {@code installFor} call remain on those children until explicitly
* overwritten.
* <p>
* {@link FlowCell} instances are most conveniently created via
* {@link swingtree.UI#AUTO_SPAN(Configurator)}:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Layout.flow()
* .withChildConstraints(
* AUTO_SPAN( it -> it.small(12).medium(6) ),
* AUTO_SPAN( it -> it.small(12).medium(6) )
* )
* }</pre>
* The intended way of creating {@link FlowCell}s is through the {@link UI#AUTO_SPAN(Configurator)} factory method!<br>
* An important edge case to consider when writing a responsive flow layout:<br>
* <b>
* If a {@link FlowCell} is passed to the responsive flow layout without
* any size specific span policies defined, it will always default to spanning 12 cells at all parent size categories!
* </b>
*
* @param childConstraints The {@link FlowCell} constraints to apply to the component's
* children in child-index order.
* @return A new {@link ForFlowLayout} with the updated child constraints.
*/
public ForFlowLayout withChildConstraints( FlowCell... childConstraints ) {
Association<Integer, FlowCell> assoc = Association.betweenSorted(Integer.class, FlowCell.class);
for ( int i = 0; i < childConstraints.length; i++ )
assoc = assoc.put(i, childConstraints[i]);
return withChildConstraints(assoc);
}
/**
* Returns a new {@link ForFlowLayout} with the {@link FlowCell} at the given child
* index replaced by the supplied value. All other child constraints and all other
* properties are copied unchanged.
* The intended way of creating {@link FlowCell}s is through the {@link UI#AUTO_SPAN(Configurator)} factory method!<br>
* <p>
* Because the underlying storage is a sparse {@link Association}, no padding
* is needed: the constraint is stored at exactly {@code index}, regardless of
* whether lower indices have entries.<br>
* Another important edge case to consider when writing a responsive flow layout:<br>
* <b>
* If a {@link FlowCell} is passed to the responsive flow layout without
* any size specific span policies defined, it will always default to spanning 12 cells at all parent size categories!
* </b>
*
* @param index The zero-based index of the child whose constraint to update.
* @param childConstraint The new {@link FlowCell} for the child at {@code index}.
* @return A new {@link ForFlowLayout} with the updated child constraint at {@code index}.
* @throws IndexOutOfBoundsException if {@code index} is negative.
*/
public ForFlowLayout withChildConstraint( int index, FlowCell childConstraint ) {
Objects.requireNonNull(childConstraint);
if ( index < 0 )
throw new IndexOutOfBoundsException("Child index must not be negative, but was: " + index);
return withChildConstraints( _childConstraints.put(index, childConstraint) );
}
/**
* Returns a new {@link ForFlowLayout} with the {@link FlowCell} at the given child
* index replaced by one built from the supplied {@link Configurator} lambda.
* The lambda receives a {@link FlowCellConf} and returns the configured version,
* matching exactly the API of {@link swingtree.UI#AUTO_SPAN(Configurator)}:
* <pre>{@code
* Layout.flow()
* .withChildConstraint(0, it -> it.small(12).medium(6).large(4))
* .withChildConstraint(1, it -> it.small(12).medium(6).large(8))
* }</pre>
* The intended way of creating {@link FlowCell}s is through the {@link UI#AUTO_SPAN(Configurator)} factory method!<br>
* An important edge case to consider when writing a responsive flow layout:<br>
* <b>
* If a {@link FlowCell} is passed to the responsive flow layout without
* any size specific span policies defined, it will always default to spanning 12 cells at all parent size categories!
* </b>
*
* @param index The zero-based index of the child whose constraint to update.
* @param cellConfig A {@link Configurator} that configures the {@link FlowCellConf}
* for the child's responsive span policy.
* @return A new {@link ForFlowLayout} with the updated child constraint at {@code index}.
* @throws IndexOutOfBoundsException if {@code index} is negative.
*/
public ForFlowLayout withChildConstraint( int index, Configurator<FlowCellConf> cellConfig ) {
return withChildConstraint( index, new FlowCell(Objects.requireNonNull(cellConfig)) );
}
/**
* Returns a new {@link ForFlowLayout} with the supplied {@link FlowCell} appended
* to the end of the existing child-constraint tuple.
* This is a convenient alternative to {@link #withChildConstraints(FlowCell...)}
* when building up constraints one at a time:
* <pre>{@code
* import static swingtree.UI.*;
* // ...
* Layout.flow()
* .withAddedChildConstraint( AUTO_SPAN(it -> it.small(12).medium(6)) )
* .withAddedChildConstraint( AUTO_SPAN(it -> it.small(12).medium(6)) )
* }</pre>
* The intended way of creating {@link FlowCell}s is through the {@link UI#AUTO_SPAN(Configurator)} factory method!<br>
* An important edge case to consider when writing a responsive flow layout:<br>
* <b>
* If a {@link FlowCell} is passed to the responsive flow layout without
* any size specific span policies defined, it will always default to spanning 12 cells at all parent size categories!
* </b>
*
* @param childConstraint The {@link FlowCell} to append as the next child constraint.
* @return A new {@link ForFlowLayout} with the constraint appended.
*/
public ForFlowLayout withAddedChildConstraint( FlowCell childConstraint ) {
Objects.requireNonNull(childConstraint);
int nextIndex = 0;
for ( Integer key : _childConstraints.keySet() )
nextIndex = key + 1;
return withChildConstraints( _childConstraints.put(nextIndex, childConstraint) );
}
/**
* Returns a new {@link ForFlowLayout} with a {@link FlowCell} built from the
* supplied {@link Configurator} lambda appended to the end of the existing
* child-constraint tuple.
* The lambda receives a {@link FlowCellConf} and returns the configured version,
* matching exactly the API of {@link swingtree.UI#AUTO_SPAN(Configurator)}:
* <pre>{@code
* Layout.flow()
* .withAddedChildConstraint( it -> it.small(12).medium(6) )
* .withAddedChildConstraint( it -> it.small(12).medium(6) )
* }</pre>
* The intended way of creating {@link FlowCell}s is through the {@link UI#AUTO_SPAN(Configurator)} factory method!<br>
* An important edge case to consider when writing a responsive flow layout:<br>
* <b>
* If a {@link FlowCell} is passed to the responsive flow layout without
* any size specific span policies defined, it will always default to spanning 12 cells at all parent size categories!
* </b>
*
* @param cellConfig A {@link Configurator} that configures the {@link FlowCellConf}
* for the appended child's responsive span policy.
* @return A new {@link ForFlowLayout} with the constraint appended.
*/
public ForFlowLayout withAddedChildConstraint( Configurator<FlowCellConf> cellConfig ) {
return withAddedChildConstraint( new FlowCell(Objects.requireNonNull(cellConfig)) );
}
// -- Object contract --
@Override public int hashCode() {
return Objects.hash( _align, _horizontalGapSize, _verticalGapSize, _childConstraints );
}
@Override
public boolean equals( @Nullable Object o ) {
if ( o == null ) return false;
if ( o == this ) return true;
if ( o.getClass() != getClass() ) return false;
ForFlowLayout other = (ForFlowLayout) o;
return _align == other._align &&
_horizontalGapSize == other._horizontalGapSize &&
_verticalGapSize == other._verticalGapSize &&
_childConstraints.equals(other._childConstraints);
}
// -- Layout installation --
/**
* Installs a {@link ResponsiveGridFlowLayout} onto the supplied component and
* applies all constraints stored in this configuration.
* <p>
* The installation proceeds in two phases:
* <ol>
* <li><b>Layout manager</b> — if no {@link ResponsiveGridFlowLayout} is currently
* installed, a new one is created with the stored alignment and gap settings.
* If one is already installed, only the properties that have changed are
* updated in-place and {@link JComponent#revalidate()} is called.</li>
* <li><b>Child constraints</b> — if the child-constraint tuple is non-empty,
* each stored {@link FlowCell} is written as a
* {@link AddConstraint} client property onto the corresponding direct child
* (using the same key that {@link ResponsiveGridFlowLayout#addLayoutComponent}
* uses, so the layout manager picks them up on the next layout pass).
* Only entries that differ from the currently stored value are written, and
* {@link JComponent#revalidate()} is called exactly once at the end if
* anything changed.</li>
* </ol>
*
* @param component The component to install the {@link ResponsiveGridFlowLayout} for.
*/
@Override
public void installFor( JComponent component ) {
// Phase 1: install / update the ResponsiveGridFlowLayout:
LayoutManager currentLayout = component.getLayout();
if ( !( currentLayout instanceof ResponsiveGridFlowLayout ) ) {
component.setLayout(new ResponsiveGridFlowLayout(_align, _horizontalGapSize, _verticalGapSize));
component.revalidate();
} else {
ResponsiveGridFlowLayout flowLayout = (ResponsiveGridFlowLayout) currentLayout;
boolean alignmentChanged = _align != flowLayout.getAlignment();
boolean horizontalGapChanged = _horizontalGapSize != flowLayout.horizontalGapSize();
boolean verticalGapChanged = _verticalGapSize != flowLayout.verticalGapSize();
if ( alignmentChanged || horizontalGapChanged || verticalGapChanged ) {
flowLayout.setAlignment(_align);
flowLayout.setHorizontalGapSize(_horizontalGapSize);
flowLayout.setVerticalGapSize(_verticalGapSize);
component.revalidate();
}
}
// Phase 2: push per-child FlowCell constraints as client properties:
if ( _childConstraints.isNotEmpty() ) {
Component[] children = component.getComponents();
boolean changed = false;
for ( Pair<Integer, FlowCell> entry : _childConstraints ) {
int i = entry.first();
if ( i < 0 || i >= children.length ) continue;
if ( !( children[i] instanceof JComponent ) )
continue;
JComponent child = (JComponent) children[i];
FlowCell desired = entry.second();
Object existing = child.getClientProperty(AddConstraint.class);
if ( !desired.equals(existing) ) {
child.putClientProperty(AddConstraint.class, desired);
changed = true;
}
}
if ( changed )
component.revalidate();
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"align=" + _align + ", " +
"hgap=" + _horizontalGapSize + ", " +
"vgap=" + _verticalGapSize + ", " +
"childConstraints=" + _childConstraints +
"]";
}
}
/**
* The {@link BorderLayoutInstaller} layout is a layout that represents
* a {@link BorderLayout} layout configuration for a component,
* which consists of the horizontal gap and vertical gap. <br>
* Whenever this layout configuration changes,
* it will create and re-install a new {@link BorderLayout} onto the component
* based on the new configuration.
*/
@Immutable
final class BorderLayoutInstaller implements Layout
{
private final int _hgap;
private final int _vgap;
BorderLayoutInstaller( int hgap, int vgap ) {
_hgap = hgap;
_vgap = vgap;
}
@Override public int hashCode() { return Objects.hash(_hgap, _vgap); }
@Override
public boolean equals( @Nullable Object o ) {
if ( o == null ) return false;
if ( o == this ) return true;
if ( o.getClass() != getClass() ) return false;
BorderLayoutInstaller other = (BorderLayoutInstaller) o;
return _hgap == other._hgap && _vgap == other._vgap;
}
/**
* Installs a {@link BorderLayout} onto the supplied component using the horizontal
* and vertical gap sizes stored in this configuration. If a {@link BorderLayout}
* is already installed, only the gap values that have changed are updated and
* {@link JComponent#revalidate()} is called to trigger a layout refresh.
*
* @param component The component to install the {@link BorderLayout} for.
*/
@Override
public void installFor( JComponent component ) {
LayoutManager currentLayout = component.getLayout();
if ( !(currentLayout instanceof BorderLayout) ) {
// We need to replace the current layout with a BorderLayout:
BorderLayout newLayout = new BorderLayout(_hgap, _vgap);
component.setLayout(newLayout);
component.revalidate();
return;
}
BorderLayout borderLayout = (BorderLayout) currentLayout;
int horizontalGap = _hgap;
int verticalGap = _vgap;
boolean horizontalGapChanged = horizontalGap != borderLayout.getHgap();
boolean verticalGapChanged = verticalGap != borderLayout.getVgap();
if ( horizontalGapChanged || verticalGapChanged ) {
borderLayout.setHgap(horizontalGap);
borderLayout.setVgap(verticalGap);
component.revalidate();
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"hgap=" + _hgap + ", " +
"vgap=" + _vgap +
"]";
}
}
/**
* The {@link GridLayoutInstaller} layout is a layout that represents
* a {@link GridLayout} layout configuration for a component,
* which consists of the number of rows, number of columns, horizontal gap and vertical gap. <br>
* Whenever this layout configuration changes,
* it will create and re-install a new {@link GridLayout} onto the component
* based on the new configuration.
*/
@Immutable
final class GridLayoutInstaller implements Layout
{
private final int _rows;
private final int _cols;
private final int _hgap;
private final int _vgap;
GridLayoutInstaller( int rows, int cols, int hgap, int vgap ) {
_rows = rows;
_cols = cols;
_hgap = hgap;
_vgap = vgap;
}
@Override public int hashCode() { return Objects.hash(_rows, _cols, _hgap, _vgap); }
@Override
public boolean equals(Object o) {
if ( o == null ) return false;
if ( o == this ) return true;
if ( o.getClass() != getClass() ) return false;
GridLayoutInstaller other = (GridLayoutInstaller) o;
return _rows == other._rows && _cols == other._cols && _hgap == other._hgap && _vgap == other._vgap;
}
/**
* Installs a {@link GridLayout} onto the supplied component using the row count,
* column count, and gap sizes stored in this configuration. If a {@link GridLayout}
* is already installed, only the properties that have changed are updated and
* {@link JComponent#revalidate()} is called to trigger a layout refresh.
*
* @param component The component to install the {@link GridLayout} for.
*/
@Override
public void installFor( JComponent component ) {
LayoutManager currentLayout = component.getLayout();
if ( !(currentLayout instanceof GridLayout) ) {
// We need to replace the current layout with a GridLayout:
GridLayout newLayout = new GridLayout(_rows, _cols, _hgap, _vgap);
component.setLayout(newLayout);
component.revalidate();
return;
}
GridLayout gridLayout = (GridLayout) currentLayout;
int rows = _rows;
int cols = _cols;
int horizontalGap = _hgap;
int verticalGap = _vgap;
boolean rowsChanged = rows != gridLayout.getRows();
boolean colsChanged = cols != gridLayout.getColumns();
boolean horizontalGapChanged = horizontalGap != gridLayout.getHgap();
boolean verticalGapChanged = verticalGap != gridLayout.getVgap();
if ( rowsChanged || colsChanged || horizontalGapChanged || verticalGapChanged ) {
gridLayout.setRows(rows);
gridLayout.setColumns(cols);
gridLayout.setHgap(horizontalGap);
gridLayout.setVgap(verticalGap);
component.revalidate();
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"rows=" + _rows + ", " +
"cols=" + _cols + ", " +
"hgap=" + _hgap + ", " +
"vgap=" + _vgap +
"]";
}
}
/**
* The {@link ForBoxLayout} layout is a layout that represents
* a {@link BoxLayout} layout configuration for a component,
* which consists of the axis. <br>
* The axis determines whether the layout will be a horizontal or vertical
* {@link BoxLayout}. <br>
* Whenever this layout configuration object changes,
* it will create and re-install a new {@link BoxLayout} onto the component
* based on the new configuration.
*/
@Immutable
final class ForBoxLayout implements Layout
{
private final int _axis;
ForBoxLayout( int axis ) { _axis = axis; }
@Override public int hashCode() { return Objects.hash(_axis); }
@Override
public boolean equals( Object o ) {
if ( o == null ) return false;
if ( o == this ) return true;
if ( o.getClass() != getClass() ) return false;
ForBoxLayout other = (ForBoxLayout) o;
return _axis == other._axis;
}
/**
* Installs a {@link BoxLayout} onto the supplied component using the axis
* stored in this configuration. If a {@link BoxLayout} with a different axis
* is already installed, a new {@link BoxLayout} is created and installed
* (since {@link BoxLayout} does not support changing the axis after construction)
* and {@link JComponent#revalidate()} is called to trigger a layout refresh.
*
* @param component The component to install the {@link BoxLayout} for.
*/
@Override
public void installFor( JComponent component ) {
LayoutManager currentLayout = component.getLayout();
if ( !( currentLayout instanceof BoxLayout ) ) {
// We need to replace the current layout with a BoxLayout:
BoxLayout newLayout = new BoxLayout( component, _axis);
component.setLayout(newLayout);
component.revalidate();
return;
}
BoxLayout boxLayout = (BoxLayout) currentLayout;
int axis = _axis;
boolean axisChanged = axis != boxLayout.getAxis();
if ( axisChanged ) {
// The BoxLayout does not have a 'setAxis' method!
// Instead, we need to create and install a new BoxLayout with the new axis:
BoxLayout newLayout = new BoxLayout( component, axis );
component.setLayout(newLayout);
component.revalidate();
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"axis=" + _axis +
"]";
}
}
}