Layout.java
package swingtree.api;
import com.google.errorprone.annotations.Immutable;
import net.miginfocom.swing.MigLayout;
import swingtree.UI;
import swingtree.style.ComponentExtension;
import swingtree.style.ComponentStyleDelegate;
import swingtree.style.StyleConf;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
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.
*/
@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; }
/**
* If you don't want to assign any layout to a component style, but you also
* don't want to pass null to the {@link ComponentStyleDelegate#layout(Layout)}
* method, you can use the no-op instance returned by this method.
*
* @return A layout that removes any existing layout from a component.
*/
static Layout none() { return Constants.NONE_LAYOUT_CONSTANT; }
/**
* This leads to the installation of the {@link MigLayout} layout manager,
* which is a powerful general purpose layout manager for Swing.
* Click <a href="http://www.miglayout.com/">here</a> for more information.
*
* @param constr The layout constraints for the layout.
* @param rowConstr The row constraints for the layout.
* @param colConstr The column constraints for the layout.
* @return A layout that uses the MigLayout.
*/
static Layout mig(
String constr,
String colConstr,
String rowConstr
) {
return new ForMigLayout( constr, colConstr, rowConstr );
}
/**
* A factory method for creating a layout that installs the {@link MigLayout}
* manager onto a component based on the supplied parameters.
* The MigLayout layout manager is a powerful general purpose layout manager for Swing.
* Click <a href="http://www.miglayout.com/">here</a> for more information.
*
* @param constr The layout constraints for the layout.
* @param rowConstr The row constraints for the layout.
* @return A layout that uses the MigLayout.
*/
static Layout mig(
String constr,
String rowConstr
) {
return new ForMigLayout( constr, "", rowConstr );
}
/**
* A factory method for creating a layout that installs the {@link MigLayout}
* manager onto a component based on the supplied parameters.
* This will effectively translate to a call to the {@link MigLayout#MigLayout(String)}
* constructor with the supplied constraints.
* 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 layout constraints for the layout.
* @return A layout that uses the MigLayout.
*/
static Layout mig( String constr ) {
return new ForMigLayout( constr, "", "" );
}
/**
* A factory method for creating a layout that installs the {@link FlowLayout}
* onto a component based on the supplied parameters.
*
* @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 for the layout.
* @param vgap The vertical gap for the layout.
* @return A layout that installs the {@link FlowLayout} onto a component.
*/
static Layout flow( UI.HorizontalAlignment align, int hgap, int vgap ) {
return new ForFlowLayout( align, hgap, vgap );
}
/**
* A factory method for creating a layout that installs the {@link FlowLayout}
* onto a component based on the supplied parameters.
*
* @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>
*
* @return A layout that installs the {@link FlowLayout} onto a component.
*/
static Layout flow( UI.HorizontalAlignment align ) {
return new ForFlowLayout( align, 5, 5 );
}
/**
* Creates a layout that installs the {@link FlowLayout}
* with a default alignment of {@link UI.HorizontalAlignment#CENTER}
* and a default gap of 5 pixels.
*
* @return A layout that installs the {@link FlowLayout} onto a component.
*/
static Layout flow() {
return new ForFlowLayout( UI.HorizontalAlignment.CENTER, 5, 5 );
}
/**
* 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 is a layout that represents the absence
* of a {@link LayoutManager} for a component.
* This means that whatever layout is currently installed for a component
* will be removed, and {@code null} will be set as the layout for the component.
* <p>
* Note that this is different from the {@link Unspecific} layout,
* which does not represent the absence of a {@link LayoutManager}
* for a component, but rather the absence of it being specified.
*/
@Immutable
final class None implements Layout
{
None(){}
@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() + "[]"; }
@Override
public void installFor( JComponent component ) {
// Contrary to the 'Unspecific' layout, this layout
// will remove any existing layout from the component:
component.setLayout(null);
}
}
/**
* The {@link ForMigLayout} layout is a layout that represents
* a {@link MigLayout} layout configuration for a component. <br>
* Whenever this layout configuration changes,
* it will create and re-install a new {@link MigLayout} onto the component
* based on the new configuration,
* which are the constraints, column constraints and row constraints.
*/
@Immutable
final class ForMigLayout implements Layout
{
private final String _constr;
private final String _colConstr;
private final String _rowConstr;
ForMigLayout( String constr, String colConstr, String rowConstr ) {
_constr = Objects.requireNonNull(constr);
_colConstr = Objects.requireNonNull(colConstr);
_rowConstr = Objects.requireNonNull(rowConstr);
}
public ForMigLayout withConstraint( String constr ) { return new ForMigLayout( constr, _colConstr, _rowConstr ); }
public ForMigLayout withRowConstraint( String rowConstr ) { return new ForMigLayout( _constr, _colConstr, rowConstr ); }
public ForMigLayout withColumnConstraint( String colConstr ) { return new ForMigLayout( _constr, colConstr, _rowConstr ); }
public ForMigLayout withComponentConstraint( String componentConstr ) { return new ForMigLayout( _constr, _colConstr, _rowConstr ); }
@Override public int hashCode() { return Objects.hash(_constr, _rowConstr, _colConstr); }
@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);
}
@Override
public void installFor( JComponent component ) {
ComponentExtension<?> extension = ComponentExtension.from(component);
StyleConf styleConf = extension.getStyle();
if ( styleConf.layoutConstraint().isPresent() ) {
// We ensure that the parent layout has the correct component constraints for the component:
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 want to compare it to the new constraints:
if ( !componentConstraints.equals(currentComponentConstraints) ) {
migLayout.setComponentConstraints(component, componentConstraints);
component.getParent().revalidate();
}
}
}
if ( !_constr.isEmpty() || !_colConstr.isEmpty() || !_rowConstr.isEmpty() ) {
// We ensure that the parent layout has the correct layout constraints for the component:
LayoutManager currentLayout = component.getLayout();
if ( !( currentLayout instanceof MigLayout ) ) {
// We need to replace the current layout with a MigLayout:
MigLayout newLayout = new MigLayout( _constr, _colConstr, _rowConstr );
component.setLayout(newLayout);
return;
}
MigLayout migLayout = (MigLayout) currentLayout;
String layoutConstraints = _constr;
String columnConstraints = _colConstr;
String rowConstraints = _rowConstr;
Object currentLayoutConstraints = migLayout.getLayoutConstraints();
Object currentColumnConstraints = migLayout.getColumnConstraints();
Object currentRowConstraints = migLayout.getRowConstraints();
boolean layoutConstraintsChanged = !layoutConstraints.equals(currentLayoutConstraints);
boolean columnConstraintsChanged = !columnConstraints.equals(currentColumnConstraints);
boolean rowConstraintsChanged = !rowConstraints.equals(currentRowConstraints);
if ( layoutConstraintsChanged || columnConstraintsChanged || rowConstraintsChanged ) {
migLayout.setLayoutConstraints(layoutConstraints);
migLayout.setColumnConstraints(columnConstraints);
migLayout.setRowConstraints(rowConstraints);
component.revalidate();
}
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"constr=" + _constr + ", " +
"colConstr=" + _colConstr + ", " +
"rowConstr=" + _rowConstr +
"]";
}
}
/**
* The {@link ForFlowLayout} layout is a layout that represents
* a {@link FlowLayout} layout configuration for a component. <br>
* Whenever this layout configuration changes,
* it will create and re-install a new {@link FlowLayout} onto the component
* based on the new configuration,
* which are the alignment, horizontal gap and vertical gap.
*/
@Immutable
final class ForFlowLayout implements Layout
{
private final int _align;
private final int _hgap;
private final int _vgap;
ForFlowLayout( UI.HorizontalAlignment align, int hgap, int vgap ) {
_align = align.forFlowLayout().orElse(FlowLayout.CENTER);
_hgap = hgap;
_vgap = vgap;
}
@Override public int hashCode() { return Objects.hash( _align, _hgap, _vgap ); }
@Override
public boolean equals( 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 && _hgap == other._hgap && _vgap == other._vgap;
}
@Override
public void installFor( JComponent component ) {
LayoutManager currentLayout = component.getLayout();
if ( !( currentLayout instanceof FlowLayout ) ) {
// We need to replace the current layout with a FlowLayout:
FlowLayout newLayout = new FlowLayout(_align, _hgap, _vgap);
component.setLayout(newLayout);
return;
}
FlowLayout flowLayout = (FlowLayout) currentLayout;
int alignment = _align;
int horizontalGap = _hgap;
int verticalGap = _vgap;
boolean alignmentChanged = alignment != flowLayout.getAlignment();
boolean horizontalGapChanged = horizontalGap != flowLayout.getHgap();
boolean verticalGapChanged = verticalGap != flowLayout.getVgap();
if ( alignmentChanged || horizontalGapChanged || verticalGapChanged ) {
flowLayout.setAlignment(alignment);
flowLayout.setHgap(horizontalGap);
flowLayout.setVgap(verticalGap);
component.revalidate();
}
}
@Override public String toString() {
return getClass().getSimpleName() + "[" +
"align=" + _align + ", " +
"hgap=" + _hgap + ", " +
"vgap=" + _vgap +
"]";
}
}
/**
* 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( 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;
}
@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);
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;
}
@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);
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;
}
@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);
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 +
"]";
}
}
}