UniformGridLayout.java
package swingtree.layout;
import swingtree.UI;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.util.Objects;
/**
* A layout manager which divides a container into a grid of cells of equal size
* and puts every component of the container into a cell of its own.
* The components fill the cells row by row, starting with the top row,
* and every component is resized to exactly the size of its cell,
* whatever size it would prefer.
* <p>
* In a SwingTree UI declaration, you install it through
* {@link swingtree.UIForAnySwing#withGridLayout(int, int, int, int)}:
* <pre>{@code
* UI.panel().withGridLayout(2, 3, 5, 5)
* .add(UI.button("1")).add(UI.button("2")).add(UI.button("3"))
* .add(UI.button("4")).add(UI.button("5"))
* }</pre>
* This panel has a grid of 2 rows and 3 columns, with a gap of 5 pixels between
* neighbouring cells. The buttons 1, 2 and 3 fill the top row from left to right,
* the buttons 4 and 5 take the first two cells of the bottom row, and the last
* cell of the bottom row stays empty. If the panel only held the buttons 1 and 2,
* they would sit side by side in a single row of 2 columns, as high as the whole
* panel, because by default the rows and columns which no component occupies are
* left out of the grid.
* <p>
* In the style API and in a reactive {@code Var<Layout>}, {@link swingtree.api.Layout#grid(int, int, int, int)}
* installs the same layout manager, and both {@code withGridLayout(..)} and {@code Layout.grid(..)}
* accept a {@link Mode}, a {@link CollapseEmpty} and an {@link OverflowGrowth} setting in front of
* the numbers of rows and columns. Outside of a SwingTree UI declaration, you install it like any
* other layout manager: {@code panel.setLayout(new UniformGridLayout(2, 3, 5, 5))}, or with every
* setting spelled out through
* {@link #UniformGridLayout(Mode, CollapseEmpty, OverflowGrowth, int, int, int, int)}.
*
* <h2>How the grid is built</h2>
*
* You declare a number of rows and a number of columns, and three settings decide what
* the layout makes of them:
* <ul>
* <li>
* The {@link Mode} decides the shape of the grid while it still has a cell for every
* component. In the default mode, {@link Mode#WRAP_AFTER_COLUMNS}, a new row starts after
* every declared number of columns: 7 components in a grid of 2 rows and 3 columns are laid
* out in 3 rows. In the mode {@link Mode#SPREAD_OVER_ROWS}, the components are spread over
* the declared rows in as many columns as they need, which is what a
* {@link java.awt.GridLayout} does.
* </li>
* <li>
* The {@link OverflowGrowth} setting takes over once there are more components than the
* declared number of rows times the declared number of columns, and decides on which axis
* the grid makes room. By default, {@link OverflowGrowth#ADD_ROWS} adds rows at the bottom
* and keeps the declared number of columns. Both modes cap a grid as soon as both declared
* numbers are greater than 0, so this setting matters in both of them; only a number of 0
* leaves an axis which grows as far as the components need and therefore never overflows.
* </li>
* <li>
* The {@link CollapseEmpty} setting decides which of the rows and columns that no component
* occupies are left out of the grid, so that the components share their space. By default,
* {@link CollapseEmpty#ROWS_AND_COLUMNS} leaves out both.
* </li>
* </ul>
* A number of rows of 0 means "as many rows as the components need", and a number of columns
* of 0 means "as many columns as the components need". So 10 components in a grid of 0 rows and
* 4 columns are laid out in 3 rows, and 10 components in a grid of 2 rows and 0 columns are laid
* out in 5 columns. The two numbers cannot both be 0, and negative numbers are not supported.
* <p>
* This is how a grid which is declared with 2 rows and 5 columns lays out 3, 8 and 11 components,
* with the default {@link OverflowGrowth#ADD_ROWS}:
* <table class="striped">
* <caption>Rows × columns laid out for a grid declared with 2 rows and 5 columns</caption>
* <tr><th>Mode</th><th>CollapseEmpty</th><th>3 components</th><th>8 components</th><th>11 components</th></tr>
* <tr><td>{@code WRAP_AFTER_COLUMNS}</td><td>{@code ROWS_AND_COLUMNS}</td><td>1 × 3</td><td>2 × 5</td><td>3 × 5</td></tr>
* <tr><td>{@code WRAP_AFTER_COLUMNS}</td><td>{@code COLUMNS}</td> <td>2 × 3</td><td>2 × 5</td><td>3 × 5</td></tr>
* <tr><td>{@code WRAP_AFTER_COLUMNS}</td><td>{@code ROWS}</td> <td>1 × 5</td><td>2 × 5</td><td>3 × 5</td></tr>
* <tr><td>{@code WRAP_AFTER_COLUMNS}</td><td>{@code NONE}</td> <td>2 × 5</td><td>2 × 5</td><td>3 × 5</td></tr>
* <tr><td>{@code SPREAD_OVER_ROWS}</td> <td>any of them</td> <td>2 × 2</td><td>2 × 4</td><td>3 × 5</td></tr>
* </table>
* Only the last column of that table holds more components than the declared 10 cells, so only
* there does the {@link OverflowGrowth} setting make a difference, and there it makes the same
* difference in both modes: once a grid overflows, the mode has nothing left to say and the growth
* policy alone decides the shape. This is how the same grid of 2 rows and 5 columns grows for 11,
* 13, 20 and 30 components, in either mode and whichever rows and columns it collapses:
* <table class="striped">
* <caption>Rows × columns laid out for an overflowing grid declared with 2 rows and 5 columns</caption>
* <tr><th>OverflowGrowth</th><th>11 components</th><th>13 components</th><th>20 components</th><th>30 components</th></tr>
* <tr><td>{@code ADD_ROWS}</td> <td>3 × 5</td><td>3 × 5</td><td>4 × 5</td> <td>6 × 5</td></tr>
* <tr><td>{@code ADD_COLUMNS}</td> <td>2 × 6</td><td>2 × 7</td><td>2 × 10</td><td>2 × 15</td></tr>
* <tr><td>{@code ADD_ROWS_AND_COLUMNS}</td><td>2 × 6</td><td>3 × 6</td><td>3 × 7</td> <td>4 × 9</td></tr>
* </table>
* <ul>
* <li>
* The default settings leave no row and no column empty. 3 components share the whole
* container in a single row, and as soon as there are enough components to fill a row, the
* grid has exactly the declared number of columns. While there are fewer components than
* declared columns, every added component makes the cells narrower. Because empty rows are
* left out, the declared number of rows only makes a difference while the declared number
* of columns is 0.
* </li>
* <li>
* With {@link CollapseEmpty#NONE}, the cells of a grid of 2 rows and 5 columns have the same
* size whether the grid holds 1 component or 10, and two such grids of the same size have
* cells which line up. With {@link CollapseEmpty#ROWS}, the cells keep their width while
* components are added, but the grid only has as many rows as the components fill.
* </li>
* <li>
* In the mode {@link Mode#SPREAD_OVER_ROWS}, an added component can change the width of every
* cell. This mode never leaves a column empty, so collapsing empty columns moves no component,
* but it can leave rows empty: 2 components in a grid of 3 rows get 1 column and only fill
* 2 of the 3 rows, and {@link CollapseEmpty#ROWS} leaves out the third one. At a UI scale
* factor of 1, a {@code UniformGridLayout} in this mode, with {@link CollapseEmpty#NONE} and
* {@link OverflowGrowth#ADD_COLUMNS}, lays out and measures a container exactly like a
* {@link java.awt.GridLayout} with the same numbers of rows and columns and the same gaps.
* </li>
* </ul>
* You choose the settings with {@link #setMode(Mode)}, {@link #setCollapseEmpty(CollapseEmpty)}
* and {@link #setOverflowGrowth(OverflowGrowth)}.
*
* <h2>The size of the cells</h2>
*
* All cells have the same size. To find their width, the layout manager takes the width
* of the container, subtracts the left and right insets and the horizontal gaps between
* the columns, divides the rest by the number of columns and rounds the result down.
* The few pixels which that rounding leaves over are split in two: half of them, rounded
* down, come before the first column, and the rest come after the last column, which
* centres the grid inside the insets.
* <p>
* For example, a container 105 pixels wide without insets, with 3 columns and a horizontal
* gap of 5 pixels, has cells which are (105 − 2 × 5) / 3 = 31.67 pixels wide, rounded down
* to 31. The cells and the gaps take up 3 × 31 + 2 × 5 = 103 pixels, and of the 2 pixels
* left over, 1 comes before the first column, so the first column starts at x = 1.
* The height of the cells is found in the same way, from the height of the container,
* the top and bottom insets, the vertical gaps and the number of rows.
* <p>
* A few more details follow from how the cells are filled:
* <ul>
* <li>
* A component which is not visible still has its cell, so hiding a component
* leaves an empty cell behind, moves none of the other components, and does not
* make a row or column empty enough to be left out.
* </li>
* <li>
* In a container with a right-to-left {@link java.awt.ComponentOrientation}, every
* row is filled from right to left, so the first component of a row is in its
* rightmost cell. The rows are still filled from the top.
* </li>
* <li>
* The preferred, minimum and maximum sizes of the components play no part in laying
* them out. The preferred and minimum sizes only matter for
* {@link #preferredLayoutSize(Container)} and {@link #minimumLayoutSize(Container)}.
* </li>
* <li>
* When a container is too small for its insets and gaps, its cells end up with
* a width or a height of 0 or less.
* </li>
* </ul>
*
* <h2>Gaps and the UI scale factor</h2>
*
* The horizontal gap is the space between two neighbouring columns, and the vertical
* gap is the space between two neighbouring rows. There is no gap between the outer
* cells and the insets of the container, so if you want space around the grid, give
* the container a border, for example through a padding in its SwingTree style.
* <p>
* You declare the gaps in pixels at a UI scale factor of 1. Every time the layout manager
* lays out or measures a container, it scales the gaps with {@link UI#scale(int)}, which
* rounds to the nearest whole pixel. So a gap of 5 is 10 pixels wide at a UI scale factor
* of 2 and 6 pixels wide at a UI scale factor of 1.25, and a change of the scale factor
* takes effect the next time the container is laid out. {@link #getHgap()} and
* {@link #getVgap()} return the gaps as you declared them.
*/
public final class UniformGridLayout implements LayoutManager {
/**
* Decides how the grid of a {@link UniformGridLayout} is built from the declared number of
* rows, the declared number of columns and the number of components in the container.
* In both modes, the grid is filled row by row, and a number of rows or columns of 0 means
* "as many as the components need". The mode only shapes a grid which still has a cell for
* every component; once there are more components than the declared numbers multiply to, the
* {@link OverflowGrowth} setting decides the shape instead. The {@link CollapseEmpty} setting
* then decides which of the rows and columns that no component occupies are left out.
* <p>
* This is how each mode builds a grid declared with 2 rows and 5 columns, as rows × columns,
* before any empty rows or columns are left out:
* <ul>
* <li>3 components: {@link #WRAP_AFTER_COLUMNS} 2 × 5, {@link #SPREAD_OVER_ROWS} 2 × 2</li>
* <li>8 components: {@link #WRAP_AFTER_COLUMNS} 2 × 5, {@link #SPREAD_OVER_ROWS} 2 × 4</li>
* <li>11 components: both modes 3 × 5, because 11 components overflow the declared 10
* cells and the default {@link OverflowGrowth#ADD_ROWS} takes the decision</li>
* </ul>
*
* @see UniformGridLayout#setMode(Mode)
*/
public enum Mode {
/**
* A new row starts after every declared number of columns, so the grid has the declared
* number of columns and at least the declared number of rows. When there are more components
* than cells, the {@link OverflowGrowth} setting decides whether the grid grows by rows,
* by columns or by both. This is the default mode.
* <p>
* A grid of 2 rows and 5 columns builds 2 rows of 5 columns for 8 components, and, with the
* default {@link OverflowGrowth#ADD_ROWS}, 3 rows of 5 columns for 11 components. While the
* grid keeps its declared number of columns, the component at index {@code i} is always in
* row {@code i / columns} and column {@code i % columns}, so adding a component never moves
* one of the components before it into another row or column.
* <p>
* If the declared number of columns is 0, the grid gets as many columns as it takes to fit
* the components into the declared number of rows, and can never overflow.
*/
WRAP_AFTER_COLUMNS,
/**
* The components are spread over the declared number of rows in as many columns as they
* need, so the declared number of columns does not shape the grid. It still counts the
* cells: once there are more components than the declared rows times the declared columns,
* the {@link OverflowGrowth} setting decides whether the grid makes room by rows, by
* columns or by both, exactly as it does in {@link #WRAP_AFTER_COLUMNS}.
* <p>
* A grid of 2 rows and 5 columns builds 2 columns for 3 components and 4 columns for 8
* components. Its 11th component overflows the declared 10 cells, and from there the growth
* policy answers: 6 columns under {@link OverflowGrowth#ADD_COLUMNS}, a third row under the
* default {@link OverflowGrowth#ADD_ROWS}. A grid of 0 rows builds its declared number of
* columns, and as many rows as its components need.
* <p>
* {@link OverflowGrowth#ADD_COLUMNS} is the policy which keeps widening the grid the way the
* {@link java.awt.GridLayout} of the JDK does, so a {@code UniformGridLayout} in this mode,
* with {@link CollapseEmpty#NONE} and that growth policy, can replace a {@code GridLayout}
* without changing where any component ends up.
*/
SPREAD_OVER_ROWS
}
/**
* Decides which of the rows and columns that no component occupies a {@link UniformGridLayout}
* leaves out of its grid. The components share the space which the left out rows and columns
* would have taken up, so they get larger cells.
* <p>
* The grid is filled row by row, so a column is empty when there are fewer components than
* columns, and a row is empty when the components do not reach it. A component which is not
* visible still occupies its cell. Here is how a grid declared with 2 rows and 5 columns, in the
* mode {@link Mode#WRAP_AFTER_COLUMNS}, lays out 3 components, as rows × columns:
* {@link #NONE} 2 × 5, {@link #COLUMNS} 2 × 3, {@link #ROWS} 1 × 5 and {@link #ROWS_AND_COLUMNS} 1 × 3.
* <p>
* In a container without components, a number of rows or columns which is collapsed is 1, and
* that single row or column is 0 pixels large, because there is no component to size it after.
*
* @see UniformGridLayout#setCollapseEmpty(CollapseEmpty)
*/
public enum CollapseEmpty {
/**
* Rows and columns are kept, even when no component occupies them. In the mode
* {@link Mode#WRAP_AFTER_COLUMNS}, the cells of a grid therefore keep their size while
* components are added, until the grid is full.
*/
NONE,
/**
* The columns which no component occupies are left out, so the grid never has more columns
* than components. A grid of 2 rows and 5 columns lays out 3 components in 2 rows of 3 columns,
* and keeps its empty second row.
*/
COLUMNS,
/**
* The rows which no component occupies are left out, so the grid only has as many rows as the
* components fill. A grid of 2 rows and 5 columns lays out 3 components in 1 row of 5 columns,
* and keeps its 2 empty columns. In the mode {@link Mode#WRAP_AFTER_COLUMNS}, the declared number
* of rows then only makes a difference while the declared number of columns is 0.
*/
ROWS,
/**
* The rows and the columns which no component occupies are left out, so no row and no column
* of the grid is ever empty. A grid of 2 rows and 5 columns lays out 3 components in 1 row of
* 3 columns, and 8 components in 2 rows of 5 columns. This is the default.
*/
ROWS_AND_COLUMNS
}
/**
* Decides how the grid of a {@link UniformGridLayout} grows when a container holds more
* components than the declared number of rows times the declared number of columns, so that
* every component still gets a cell of its own.
* <p>
* A grid can overflow whenever its declared number of rows and its declared number of columns
* are both greater than 0, in either {@link Mode}: both modes then have a fixed number of cells
* to run out of. A number of rows or columns of 0 means "as many as the components need", which
* leaves that axis free to grow on its own, so such a grid never overflows and this setting
* never changes it.
* <p>
* The {@link Mode} shapes the grid up to that point and this setting shapes it from there on, so
* two grids which differ only in their mode are laid out alike once both have overflowed. This is
* how a grid declared with 2 rows and 5 columns, which has 10 cells, grows for 13 components, as
* rows × columns: {@link #ADD_ROWS} 3 × 5, {@link #ADD_COLUMNS} 2 × 7 and
* {@link #ADD_ROWS_AND_COLUMNS} 3 × 6. The grid grows before the {@link CollapseEmpty} setting
* leaves out the rows and columns which no component occupies.
*
* @see UniformGridLayout#setOverflowGrowth(OverflowGrowth)
*/
public enum OverflowGrowth {
/**
* Rows are added at the bottom until every component has a cell, and the grid keeps its
* declared number of columns. A grid of 2 rows and 5 columns lays out 13 components in
* 3 rows of 5 columns, and 30 components in 6 rows of 5 columns. This is the default.
* <p>
* In the mode {@link Mode#WRAP_AFTER_COLUMNS}, this is the only setting under which the
* component at index {@code i} stays in row {@code i / columns} and column {@code i % columns}
* however many components there are, so it is the only one under which adding a component
* never moves one of the components before it into another row or column.
*/
ADD_ROWS,
/**
* Columns are added at the right until every component has a cell, and the grid keeps its
* declared number of rows. A grid of 2 rows and 5 columns lays out 13 components in 2 rows
* of 7 columns, and 30 components in 2 rows of 15 columns.
* <p>
* This is the one policy which keeps making room the way a {@link java.awt.GridLayout} makes
* room, so it is the growth policy the mode {@link Mode#SPREAD_OVER_ROWS} needs, alongside
* {@link CollapseEmpty#NONE}, to lay out components exactly like one. The mode still decides
* the grid below the declared number of cells: with {@link Mode#WRAP_AFTER_COLUMNS} a grid of
* 2 rows and 5 columns lays out 3 components in 5 columns, and with
* {@link Mode#SPREAD_OVER_ROWS} it lays them out in 2.
*/
ADD_COLUMNS,
/**
* Rows and columns are added one at a time, each time the one which keeps the ratio of rows
* to columns closest to the declared ratio, until every component has a cell. The grid
* therefore keeps its shape as closely as whole rows and columns allow, and so do its cells:
* the container does not change size while the grid grows, so a cell of a grid of {@code r}
* rows and {@code c} columns is {@code width / c} by {@code height / r} large, and holding
* {@code r / c} at the declared ratio holds the shape of every cell as well.
* <p>
* A grid of 2 rows and 5 columns lays out 13 components in 3 rows of 6 columns, and 30
* components in 4 rows of 9 columns. When the declared numbers scale up evenly, the ratio is
* kept exactly and no cell is left over: a grid of 1 row and 4 columns lays out 400
* components, 100 times its 4 cells, in exactly 10 rows of 40 columns.
* <p>
* Growing both numbers can leave more empty cells behind than {@link #ADD_ROWS} or
* {@link #ADD_COLUMNS} do, because the last row of the grid is the one which absorbs the
* difference: 13 components in 3 rows of 6 columns leave 5 cells of the last row empty.
*/
ADD_ROWS_AND_COLUMNS
}
int hgap;
int vgap;
int rows;
int cols;
Mode mode = Mode.WRAP_AFTER_COLUMNS;
CollapseEmpty collapseEmpty = CollapseEmpty.ROWS_AND_COLUMNS;
OverflowGrowth overflowGrowth = OverflowGrowth.ADD_ROWS;
/**
* Creates a grid layout with a single row, a column for every component and no gaps,
* which places the components of the container side by side in cells of equal width.
* This is the same as {@code new UniformGridLayout(1, 0, 0, 0)}.
*/
public UniformGridLayout() {
this(1, 0, 0, 0);
}
/**
* Creates a grid layout with the given number of rows and columns and no gaps between the
* cells, in the mode {@link Mode#WRAP_AFTER_COLUMNS}, which collapses
* {@link CollapseEmpty#ROWS_AND_COLUMNS}. A number of 0 means "as many as the components need",
* so {@code new UniformGridLayout(0, 3)} arranges the components in 3 columns and as many rows
* as it takes to hold all of them, and arranges 2 components in a single row of 2 columns.
*
* @param rows The number of rows, or 0 for as many rows as the components need.
* @param cols The number of columns, or 0 for as many columns as the components need.
* @throws IllegalArgumentException If both {@code rows} and {@code cols} are 0.
*/
public UniformGridLayout(int rows, int cols) {
this(rows, cols, 0, 0);
}
/**
* Creates a grid layout with the given number of rows and columns and the given gaps between
* neighbouring cells, in the mode {@link Mode#WRAP_AFTER_COLUMNS}, which collapses
* {@link CollapseEmpty#ROWS_AND_COLUMNS}. A number of 0 means "as many as the components need",
* so {@code new UniformGridLayout(2, 0, 5, 5)} spreads the components over 2 rows, in as many
* columns as it takes to hold all of them, with 5 pixels between neighbouring cells, and leaves
* out the second row for a single component. The gaps are scaled with {@link UI#scale(int)}
* every time the container is laid out or measured.
*
* @param rows The number of rows, or 0 for as many rows as the components need.
* @param cols The number of columns, or 0 for as many columns as the components need.
* @param hgap The space between two neighbouring columns, in pixels at a UI scale factor of 1.
* @param vgap The space between two neighbouring rows, in pixels at a UI scale factor of 1.
* @throws IllegalArgumentException If both {@code rows} and {@code cols} are 0.
*/
public UniformGridLayout(int rows, int cols, int hgap, int vgap) {
if ((rows == 0) && (cols == 0)) {
throw new IllegalArgumentException("rows and cols cannot both be zero");
}
this.rows = rows;
this.cols = cols;
this.hgap = hgap;
this.vgap = vgap;
}
/**
* Creates a grid layout with every one of its settings spelled out: how the grid is built from
* the given numbers of rows and columns, how it grows when it holds more components than cells,
* which of its empty rows and columns it leaves out, and the gaps between neighbouring cells.
* The other constructors leave the three settings at {@link Mode#WRAP_AFTER_COLUMNS},
* {@link CollapseEmpty#ROWS_AND_COLUMNS} and {@link OverflowGrowth#ADD_ROWS}.
* <p>
* For example,
* {@code new UniformGridLayout(Mode.WRAP_AFTER_COLUMNS, CollapseEmpty.NONE, OverflowGrowth.ADD_COLUMNS, 2, 5, 5, 5)}
* keeps all 10 cells of a grid of 2 rows and 5 columns for 3 components, and widens the grid to
* 2 rows of 7 columns for 13 components, with 5 pixels between neighbouring cells at a UI scale
* factor of 1.
*
* @param mode How the grid is built from the numbers of rows and columns.
* @param collapseEmpty Which of the rows and columns that no component occupies are left out.
* @param overflowGrowth How the grid grows when it holds more components than cells.
* @param rows The number of rows, or 0 for as many rows as the components need.
* @param cols The number of columns, or 0 for as many columns as the components need.
* @param hgap The space between two neighbouring columns, in pixels at a UI scale factor of 1.
* @param vgap The space between two neighbouring rows, in pixels at a UI scale factor of 1.
* @throws IllegalArgumentException If both {@code rows} and {@code cols} are 0.
* @throws NullPointerException If {@code mode}, {@code collapseEmpty} or {@code overflowGrowth} is {@code null}.
*/
public UniformGridLayout(
Mode mode,
CollapseEmpty collapseEmpty,
OverflowGrowth overflowGrowth,
int rows,
int cols,
int hgap,
int vgap
) {
this(rows, cols, hgap, vgap);
this.mode = Objects.requireNonNull(mode);
this.collapseEmpty = Objects.requireNonNull(collapseEmpty);
this.overflowGrowth = Objects.requireNonNull(overflowGrowth);
}
/**
* Returns the number of rows you gave this layout, where 0 means "as many rows as the
* components need". The container can be laid out in a different number of rows: in the mode
* {@link Mode#WRAP_AFTER_COLUMNS}, rows are added when there are more components than cells,
* and when empty rows are collapsed, the rows which no component reaches are left out.
*
* @return The declared number of rows, which may be 0.
*/
public int getRows() {
return rows;
}
/**
* Sets the number of rows of the grid, where 0 means "as many rows as the components need".
* In the mode {@link Mode#WRAP_AFTER_COLUMNS} with empty rows collapsed, which are the default
* settings, this number only makes a difference while the number of columns is 0.
* <p>
* The number of rows and the number of columns cannot both be 0, and this method checks
* that against the number of columns this layout has at the moment it is called. So to
* switch a layout of 0 rows and 3 columns to 2 rows and 0 columns, call {@code setRows(2)}
* first and {@code setColumns(0)} second, and to switch it back, call {@code setColumns(3)}
* first and {@code setRows(0)} second.
* <p>
* This method does not lay out the container again. Call
* {@link java.awt.Component#revalidate()} on the container to apply the new number.
*
* @param rows The number of rows, or 0 for as many rows as the components need.
* @throws IllegalArgumentException If {@code rows} is 0 while the number of columns is 0 as well.
*/
public void setRows(int rows) {
if ((rows == 0) && (this.cols == 0)) {
throw new IllegalArgumentException("rows and cols cannot both be zero");
}
this.rows = rows;
}
/**
* Returns the number of columns you gave this layout, where 0 means "as many columns as the
* components need". The container can be laid out in a different number of columns: in the mode
* {@link Mode#SPREAD_OVER_ROWS}, the number of columns is worked out from the number of rows
* whenever that is greater than 0, and when empty columns are collapsed, the grid never has more
* columns than components.
*
* @return The declared number of columns, which may be 0.
*/
public int getColumns() {
return cols;
}
/**
* Sets the number of columns of the grid, where 0 means "as many columns as the components need".
* In the mode {@link Mode#SPREAD_OVER_ROWS}, this number is ignored while the number of rows is
* greater than 0, and when empty columns are collapsed, the grid never has more columns than
* components.
* <p>
* The number of rows and the number of columns cannot both be 0, and this method checks
* that against the number of rows this layout has at the moment it is called. So to switch
* a layout of 2 rows and 0 columns to 0 rows and 3 columns, call {@code setColumns(3)} first
* and {@code setRows(0)} second, and to switch it back, call {@code setRows(2)} first and
* {@code setColumns(0)} second.
* <p>
* This method does not lay out the container again. Call
* {@link java.awt.Component#revalidate()} on the container to apply the new number.
*
* @param cols The number of columns, or 0 for as many columns as the components need.
* @throws IllegalArgumentException If {@code cols} is 0 while the number of rows is 0 as well.
*/
public void setColumns(int cols) {
if ((cols == 0) && (this.rows == 0)) {
throw new IllegalArgumentException("rows and cols cannot both be zero");
}
this.cols = cols;
}
/**
* Returns the mode which decides how the grid is built from the declared numbers of rows and
* columns. See {@link Mode} for what each mode does.
*
* @return The mode of this layout, which is {@link Mode#WRAP_AFTER_COLUMNS} unless you changed it.
*/
public Mode getMode() {
return mode;
}
/**
* Sets the mode which decides how the grid is built from the declared numbers of rows and columns,
* for as long as it still has a cell for every component:
* <ul>
* <li>{@link Mode#WRAP_AFTER_COLUMNS}, the default, starts a new row after every declared
* number of columns.</li>
* <li>{@link Mode#SPREAD_OVER_ROWS} spreads the components over the declared rows in as many
* columns as they need, like a {@link java.awt.GridLayout}.</li>
* </ul>
* A grid of 2 rows and 5 columns builds 2 rows of 5 columns for 8 components in the first mode,
* and 2 rows of 4 columns in the second. Once a grid holds more components than its declared
* numbers multiply to, the mode no longer decides its shape and
* {@link #setOverflowGrowth(OverflowGrowth)} does. The layout manager reads the mode every time it lays out
* or measures the container, but this method does not lay out the container again. Call
* {@link java.awt.Component#revalidate()} on the container to apply the new mode.
*
* @param mode The mode of this layout.
* @throws NullPointerException If {@code mode} is {@code null}, in which case the mode stays as it was.
*/
public void setMode(Mode mode) {
this.mode = Objects.requireNonNull(mode);
}
/**
* Returns the setting which decides which of the rows and columns that no component occupies
* are left out of the grid. See {@link CollapseEmpty} for what each setting does.
*
* @return The setting of this layout, which is {@link CollapseEmpty#ROWS_AND_COLUMNS} unless you changed it.
*/
public CollapseEmpty getCollapseEmpty() {
return collapseEmpty;
}
/**
* Sets which of the rows and columns that no component occupies are left out of the grid:
* {@link CollapseEmpty#NONE}, {@link CollapseEmpty#COLUMNS}, {@link CollapseEmpty#ROWS}, or
* {@link CollapseEmpty#ROWS_AND_COLUMNS}, which is the default. In the mode
* {@link Mode#WRAP_AFTER_COLUMNS}, a grid of 2 rows and 5 columns lays out 3 components in
* 2 × 5, 2 × 3, 1 × 5 and 1 × 3 rows × columns with these settings.
* <p>
* The layout manager reads the setting every time it lays out or measures the container, but
* this method does not lay out the container again. Call {@link java.awt.Component#revalidate()}
* on the container to apply the new setting.
*
* @param collapseEmpty Which empty rows and columns this layout leaves out.
* @throws NullPointerException If {@code collapseEmpty} is {@code null}, in which case the setting stays as it was.
*/
public void setCollapseEmpty(CollapseEmpty collapseEmpty) {
this.collapseEmpty = Objects.requireNonNull(collapseEmpty);
}
/**
* Returns the setting which decides how the grid grows when the container holds more components
* than cells. See {@link OverflowGrowth} for what each setting does, and for the grids which
* can overflow at all.
*
* @return The setting of this layout, which is {@link OverflowGrowth#ADD_ROWS} unless you changed it.
*/
public OverflowGrowth getOverflowGrowth() {
return overflowGrowth;
}
/**
* Sets how the grid grows when the container holds more components than the declared number of
* rows times the declared number of columns: {@link OverflowGrowth#ADD_ROWS}, which is the default,
* {@link OverflowGrowth#ADD_COLUMNS}, or {@link OverflowGrowth#ADD_ROWS_AND_COLUMNS}, which keeps
* the ratio of rows to columns as close to the declared ratio as whole rows and columns allow.
* A grid of 2 rows and 5 columns lays out 13 components in 3 × 5, 2 × 7 and 3 × 6 rows × columns
* with these settings.
* <p>
* This setting makes a difference in both modes, but only while the declared numbers of rows and
* columns are both greater than 0, because a grid with a number of 0 grows that axis as far as the
* components need and can therefore never hold more components than it has cells.
* <p>
* The layout manager reads the setting every time it lays out or measures the container, but
* this method does not lay out the container again. Call {@link java.awt.Component#revalidate()}
* on the container to apply the new setting.
*
* @param overflowGrowth How this layout grows a grid which holds more components than cells.
* @throws NullPointerException If {@code overflowGrowth} is {@code null}, in which case the setting stays as it was.
*/
public void setOverflowGrowth(OverflowGrowth overflowGrowth) {
this.overflowGrowth = Objects.requireNonNull(overflowGrowth);
}
private boolean collapsesEmptyColumns() {
return collapseEmpty == CollapseEmpty.COLUMNS || collapseEmpty == CollapseEmpty.ROWS_AND_COLUMNS;
}
private boolean collapsesEmptyRows() {
return collapseEmpty == CollapseEmpty.ROWS || collapseEmpty == CollapseEmpty.ROWS_AND_COLUMNS;
}
private static final int ROW_COUNT = 0;
private static final int COLUMN_COUNT = 1;
private int[] gridFor(int ncomponents) {
int nrows = rows;
int ncols = cols;
if (rows > 0 && cols > 0) {
if ((long) rows * cols < ncomponents) {
switch (overflowGrowth) {
case ADD_COLUMNS:
ncols = (ncomponents + rows - 1) / rows;
break;
case ADD_ROWS_AND_COLUMNS:
while ((long) nrows * ncols < ncomponents) {
long withOneMoreRow = Math.abs((long) (nrows + 1) * cols - (long) rows * ncols) * (ncols + 1);
long withOneMoreColumn = Math.abs((long) nrows * cols - (long) rows * (ncols + 1)) * ncols;
if (withOneMoreRow <= withOneMoreColumn) {
nrows++;
} else {
ncols++;
}
}
break;
default:
nrows = (ncomponents + cols - 1) / cols;
}
} else if (mode == Mode.SPREAD_OVER_ROWS) {
ncols = (ncomponents + rows - 1) / rows;
}
} else if (rows > 0) {
ncols = (ncomponents + rows - 1) / rows;
} else {
nrows = ncols > 0 ? (ncomponents + ncols - 1) / ncols : 0;
}
if (collapsesEmptyColumns()) {
ncols = Math.max(Math.min(ncols, ncomponents), 1);
}
if (collapsesEmptyRows()) {
nrows = ncols > 0 ? Math.max((ncomponents + ncols - 1) / ncols, 1) : 1;
}
int[] grid = new int[2];
grid[ROW_COUNT] = nrows;
grid[COLUMN_COUNT] = ncols;
return grid;
}
/**
* Returns the space between two neighbouring columns as you declared it, in pixels at
* a UI scale factor of 1. In a laid out container, the space is this number scaled with
* {@link UI#scale(int)}.
*
* @return The declared horizontal gap.
*/
public int getHgap() {
return hgap;
}
/**
* Sets the space between two neighbouring columns, in pixels at a UI scale factor of 1.
* The layout manager scales it with {@link UI#scale(int)} every time it lays out or
* measures the container, and puts no gap between the outer columns and the insets of the
* container. A negative gap is not rejected, and makes neighbouring cells overlap.
* <p>
* This method does not lay out the container again. Call
* {@link java.awt.Component#revalidate()} on the container to apply the new gap.
*
* @param hgap The horizontal gap, in pixels at a UI scale factor of 1.
*/
public void setHgap(int hgap) {
this.hgap = hgap;
}
/**
* Returns the space between two neighbouring rows as you declared it, in pixels at
* a UI scale factor of 1. In a laid out container, the space is this number scaled with
* {@link UI#scale(int)}.
*
* @return The declared vertical gap.
*/
public int getVgap() {
return vgap;
}
/**
* Sets the space between two neighbouring rows, in pixels at a UI scale factor of 1.
* The layout manager scales it with {@link UI#scale(int)} every time it lays out or
* measures the container, and puts no gap between the outer rows and the insets of the
* container. A negative gap is not rejected, and makes neighbouring cells overlap.
* <p>
* This method does not lay out the container again. Call
* {@link java.awt.Component#revalidate()} on the container to apply the new gap.
*
* @param vgap The vertical gap, in pixels at a UI scale factor of 1.
*/
public void setVgap(int vgap) {
this.vgap = vgap;
}
/**
* Does nothing. This layout manager keeps no information about individual components:
* every time it lays out or measures a container, it asks the container for its components
* and places them in the order of their index in the container.
*
* @param name The name the component was added with, which this layout manager ignores.
* @param comp The component which was added to the container.
*/
@Override
public void addLayoutComponent(String name, Component comp) {
}
/**
* Does nothing. This layout manager keeps no information about individual components,
* so a removed component simply no longer takes a cell the next time the container is laid out.
*
* @param comp The component which was removed from the container.
*/
@Override
public void removeLayoutComponent(Component comp) {
}
/**
* Computes the size a container needs to give every component a cell as wide as the widest
* preferred width, and as tall as the tallest preferred height, among its components.
* <p>
* The widest preferred width and the tallest preferred height may belong to two different
* components, and components which are not visible count as well. The preferred width of the
* container is the cell width times the number of columns, plus the horizontal gaps between
* the columns, plus the left and right insets. The preferred height is computed in the same
* way from the cell height, the number of rows, the vertical gaps and the top and bottom insets.
* The gaps are scaled with {@link UI#scale(int)}.
* <p>
* For example, 3 components which each prefer 30 × 20 pixels, in a grid of 2 rows and 5 columns
* with gaps of 5 pixels, at a UI scale factor of 1 and in a container without insets, prefer
* <ul>
* <li>3 × 30 + 2 × 5 = 100 by 20 pixels with the default settings, which lay them out in 1 row of 3 columns,</li>
* <li>5 × 30 + 4 × 5 = 170 by 2 × 20 + 1 × 5 = 45 pixels with {@link CollapseEmpty#NONE},</li>
* <li>100 by 45 pixels with {@link CollapseEmpty#COLUMNS}, and 170 by 20 pixels with {@link CollapseEmpty#ROWS},</li>
* <li>2 × 30 + 1 × 5 = 65 by 45 pixels in the mode {@link Mode#SPREAD_OVER_ROWS} with {@link CollapseEmpty#NONE}.</li>
* </ul>
* The numbers of rows and columns are worked out exactly like {@link #layoutContainer(Container)}
* works them out, so a container laid out at its preferred size gives every component a cell of
* exactly that width and height.
* <p>
* In a container without components, a number of rows or columns which is worked out from the
* components is 0, and the gaps between 0 cells add up to minus one gap: an empty container with
* a grid of 2 rows and 0 columns, a horizontal gap of 5 pixels and {@link CollapseEmpty#NONE}
* prefers a width of −5 pixels, exactly like it would with a {@link java.awt.GridLayout}.
* A number of rows or columns which is collapsed is 1 instead, so with the default settings, an
* empty container prefers exactly the size of its insets.
*
* @param parent The container to compute the preferred size for.
* @return The preferred size of {@code parent}, including its insets.
*/
@Override
public Dimension preferredLayoutSize(Container parent) {
synchronized (parent.getTreeLock()) {
Insets insets = parent.getInsets();
int ncomponents = parent.getComponentCount();
int[] grid = gridFor(ncomponents);
int nrows = grid[ROW_COUNT];
int ncols = grid[COLUMN_COUNT];
int horizontalGap = UI.scale(hgap);
int verticalGap = UI.scale(vgap);
int w = 0;
int h = 0;
for (int i = 0 ; i < ncomponents ; i++) {
Component comp = parent.getComponent(i);
Dimension d = comp.getPreferredSize();
if (w < d.width) {
w = d.width;
}
if (h < d.height) {
h = d.height;
}
}
return new Dimension(insets.left + insets.right + ncols*w + (ncols-1)*horizontalGap,
insets.top + insets.bottom + nrows*h + (nrows-1)*verticalGap);
}
}
/**
* Computes the size a container needs to give every component a cell as wide as the widest
* minimum width, and as tall as the tallest minimum height, among its components.
* <p>
* The widest minimum width and the tallest minimum height may belong to two different
* components, and components which are not visible count as well. The minimum width of the
* container is the cell width times the number of columns, plus the horizontal gaps between
* the columns, plus the left and right insets. The minimum height is computed in the same
* way from the cell height, the number of rows, the vertical gaps and the top and bottom insets.
* The gaps are scaled with {@link UI#scale(int)}, and the numbers of rows and columns are worked
* out exactly like {@link #layoutContainer(Container)} works them out.
* <p>
* {@link #layoutContainer(Container)} does not enforce this size. A container which is smaller
* is laid out all the same, and then at least one of its components gets a cell smaller than
* its minimum size.
*
* @param parent The container to compute the minimum size for.
* @return The minimum size of {@code parent}, including its insets.
*/
@Override
public Dimension minimumLayoutSize(Container parent) {
synchronized (parent.getTreeLock()) {
Insets insets = parent.getInsets();
int ncomponents = parent.getComponentCount();
int[] grid = gridFor(ncomponents);
int nrows = grid[ROW_COUNT];
int ncols = grid[COLUMN_COUNT];
int horizontalGap = UI.scale(hgap);
int verticalGap = UI.scale(vgap);
int w = 0;
int h = 0;
for (int i = 0 ; i < ncomponents ; i++) {
Component comp = parent.getComponent(i);
Dimension d = comp.getMinimumSize();
if (w < d.width) {
w = d.width;
}
if (h < d.height) {
h = d.height;
}
}
return new Dimension(insets.left + insets.right + ncols*w + (ncols-1)*horizontalGap,
insets.top + insets.bottom + nrows*h + (nrows-1)*verticalGap);
}
}
/**
* Moves and resizes every component of the container into its cell of the grid.
* <p>
* The grid has the declared numbers of rows and columns, with these exceptions:
* <ul>
* <li>A number of rows or columns of 0 is worked out from the number of components.</li>
* <li>In the mode {@link Mode#SPREAD_OVER_ROWS}, while the number of rows is greater than 0,
* the number of columns is worked out from the number of rows and the number of components.</li>
* <li>When there are more components than the declared number of rows times the declared
* number of columns, the grid grows on the axis the {@link OverflowGrowth} setting names,
* in either mode.</li>
* <li>The rows and columns which no component occupies are then left out, as far as the
* {@link CollapseEmpty} setting says so.</li>
* </ul>
* All cells are equally large. Their width is the width inside the insets, minus the horizontal
* gaps scaled with {@link UI#scale(int)}, divided by the number of columns and rounded down, and
* their height is found in the same way from the rows. The pixels which the rounding leaves over
* are split: half of them, rounded down, come before the first column and above the first row.
* <p>
* Every component, visible or not, is given exactly the bounds of its cell. The cells are filled
* row by row from the top, and within a row from left to right, or from right to left in a
* container with a right-to-left {@link java.awt.ComponentOrientation}. A container without
* components is left untouched.
*
* @param parent The container whose components are laid out.
*/
@Override
public void layoutContainer(Container parent) {
synchronized (parent.getTreeLock()) {
Insets insets = parent.getInsets();
int ncomponents = parent.getComponentCount();
boolean ltr = parent.getComponentOrientation().isLeftToRight();
if (ncomponents == 0) {
return;
}
int[] grid = gridFor(ncomponents);
int nrows = grid[ROW_COUNT];
int ncols = grid[COLUMN_COUNT];
int horizontalGap = UI.scale(hgap);
int verticalGap = UI.scale(vgap);
// 4370316. To position components in the center we should:
// 1. get an amount of extra space within Container
// 2. incorporate half of that value to the left/top position
// Note that we use trancating division for widthOnComponent
// The reminder goes to extraWidthAvailable
int totalGapsWidth = (ncols - 1) * horizontalGap;
int widthWOInsets = parent.getWidth() - (insets.left + insets.right);
int widthOnComponent = (widthWOInsets - totalGapsWidth) / ncols;
int extraWidthAvailable = (widthWOInsets - (widthOnComponent * ncols + totalGapsWidth)) / 2;
int totalGapsHeight = (nrows - 1) * verticalGap;
int heightWOInsets = parent.getHeight() - (insets.top + insets.bottom);
int heightOnComponent = (heightWOInsets - totalGapsHeight) / nrows;
int extraHeightAvailable = (heightWOInsets - (heightOnComponent * nrows + totalGapsHeight)) / 2;
if (ltr) {
for (int c = 0, x = insets.left + extraWidthAvailable; c < ncols ; c++, x += widthOnComponent + horizontalGap) {
for (int r = 0, y = insets.top + extraHeightAvailable; r < nrows ; r++, y += heightOnComponent + verticalGap) {
int i = r * ncols + c;
if (i < ncomponents) {
parent.getComponent(i).setBounds(x, y, widthOnComponent, heightOnComponent);
}
}
}
} else {
for (int c = 0, x = (parent.getWidth() - insets.right - widthOnComponent) - extraWidthAvailable; c < ncols ; c++, x -= widthOnComponent + horizontalGap) {
for (int r = 0, y = insets.top + extraHeightAvailable; r < nrows ; r++, y += heightOnComponent + verticalGap) {
int i = r * ncols + c;
if (i < ncomponents) {
parent.getComponent(i).setBounds(x, y, widthOnComponent, heightOnComponent);
}
}
}
}
}
}
/**
* Returns the declared settings of this layout manager in a line of text, for example
* {@code swingtree.layout.UniformGridLayout[hgap=5,vgap=5,rows=2,cols=5,mode=WRAP_AFTER_COLUMNS,collapseEmpty=ROWS_AND_COLUMNS,overflowGrowth=ADD_ROWS]}.
* The gaps in it are the declared gaps, not the scaled ones.
*
* @return A text with the class name, the gaps, the numbers of rows and columns, the mode,
* which empty rows and columns are left out, and how the grid grows when it overflows.
*/
@Override
public String toString() {
return getClass().getName() + "[hgap=" + hgap + ",vgap=" + vgap +
",rows=" + rows + ",cols=" + cols +
",mode=" + mode + ",collapseEmpty=" + collapseEmpty +
",overflowGrowth=" + overflowGrowth + "]";
}
}