com.privatejgoodies.forms.layout.FormLayout Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2013 JGoodies Software GmbH. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.privatejgoodies.forms.layout;
import static com.privatejgoodies.common.base.Preconditions.checkNotNull;
import static com.privatejgoodies.common.base.Preconditions.checkState;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JComponent;
import com.privatejgoodies.common.base.Objects;
/**
* FormLayout is a powerful, flexible and precise general purpose layout manager. It aligns
* components vertically and horizontally in a dynamic rectangular grid of cells, with each
* component occupying one or more cells. A
* whitepaper
* about the FormLayout ships with the product documentation and is available
* online.
*
* To use FormLayout you first define the grid by specifying the columns and rows. In a second step
* you add components to the grid. You can specify columns and rows via human-readable String
* descriptions or via arrays of {@link ColumnSpec} and {@link RowSpec} instances.
*
* Each component managed by a FormLayout is associated with an instance of {@link CellConstraints}.
* The constraints object specifies where a component should be located on the form's grid and how
* the component should be positioned. In addition to its constraints object the {@code FormLayout}
* also considers each component's minimum and preferred sizes in order to determine a component's
* size.
*
* FormLayout has been designed to work with non-visual builders that help you specify the layout
* and fill the grid. For example, the com.privatejgoodies.forms.builder.ButtonBarBuilder assists
* you in building button bars; it creates a standardized FormLayout and provides a minimal API that
* specializes in adding buttons and Actions. Other builders can create frequently used panel
* design, for example a form that consists of rows of label-component pairs.
*
* FormLayout has been prepared to work with different types of sizes as defined by the {@link Size}
* interface.
*
* Example 1 (Plain FormLayout):
* The following example creates a panel with 3 data columns and 3 data rows; the columns and rows
* are specified before components are added to the form.
*
* FormLayout layout = new FormLayout(
* "right:pref, 6dlu, 50dlu, 4dlu, default", // columns
* "pref, 3dlu, pref, 3dlu, pref"); // rows
*
* JPanel panel = new JPanel(layout);
* panel.add(new JLabel("Label1"), CC.xy (1, 1));
* panel.add(new JTextField(), CC.xywh(3, 1, 3, 1));
* panel.add(new JLabel("Label2"), CC.xy (1, 3));
* panel.add(new JTextField(), CC.xy (3, 3));
* panel.add(new JLabel("Label3"), CC.xy (1, 5));
* panel.add(new JTextField(), CC.xy (3, 5));
* panel.add(new JButton("/u2026"), CC.xy (5, 5));
* return panel;
*
*
* Example 2 (Using PanelBuilder):
* This example creates the same panel as above using the PanelBuilder to add components to the
* form.
*
* FormLayout layout = new FormLayout(
* "right:pref, 6dlu, 50dlu, 4dlu, default", // columns
* "pref, 3dlu, pref, 3dlu, pref"); // rows
*
* PanelBuilder builder = new PanelBuilder(layout);
* builder.addLabel("Label1", CC.xy (1, 1));
* builder.add(new JTextField(), CC.xywh(3, 1, 3, 1));
* builder.addLabel("Label2", CC.xy (1, 3));
* builder.add(new JTextField(), CC.xy (3, 3));
* builder.addLabel("Label3", CC.xy (1, 5));
* builder.add(new JTextField(), CC.xy (3, 5));
* builder.add(new JButton("/u2026"), CC.xy (5, 5));
* return builder.getPanel();
*
*
* Example 3 (Using DefaultFormBuilder):
* This example utilizes the com.privatejgoodies.forms.builder.DefaultFormBuilder that ships with
* the source distribution.
*
* FormLayout layout = new FormLayout(
* "right:pref, 6dlu, 50dlu, 4dlu, default"); // 5 columns; add rows later
*
* DefaultFormBuilder builder = new DefaultFormBuilder(layout);
* builder.append("Label1", new JTextField(), 3);
* builder.append("Label2", new JTextField());
* builder.append("Label3", new JTextField());
* builder.append(new JButton("/u2026"));
* return builder.getPanel();
*
*
* @author Karsten Lentzsch
* @version $Revision: 1.30 $
*
* @see ColumnSpec
* @see RowSpec
* @see CellConstraints see com.privatejgoodies.forms.builder.AbstractFormBuilder
* @see com.privatejgoodies.forms.layout.FormSpecs
* @see Size
* @see Sizes See com.privatejgoodies.forms.builder.DefaultFormBuilder
*/
public final class FormLayout implements LayoutManager2, Serializable {
// Instance Fields ********************************************************
/**
* Holds the column specifications.
*
* @see ColumnSpec
* @see #getColumnCount()
* @see #getColumnSpec(int)
* @see #appendColumn(ColumnSpec)
* @see #insertColumn(int, ColumnSpec)
* @see #removeColumn(int)
*/
private final List colSpecs;
/**
* Holds the row specifications.
*
* @see RowSpec
* @see #getRowCount()
* @see #getRowSpec(int)
* @see #appendRow(RowSpec)
* @see #insertRow(int, RowSpec)
* @see #removeRow(int)
*/
private final List rowSpecs;
/**
* Holds the column groups as an array of arrays of column indices.
*
* @see #getColumnGroups()
* @see #setColumnGroups(int[][])
* @see #addGroupedColumn(int)
*/
private int[][] colGroupIndices;
/**
* Holds the row groups as an array of arrays of row indices.
*
* @see #getRowGroups()
* @see #setRowGroups(int[][])
* @see #addGroupedRow(int)
*/
private int[][] rowGroupIndices;
/**
* Maps components to their associated {@code CellConstraints}.
*
* @see CellConstraints
* @see #getConstraints(Component)
* @see #setConstraints(Component, CellConstraints)
*/
private final Map constraintMap;
private boolean honorsVisibility = true;
// Fields used by the Layout Algorithm ************************************
/**
* Holds the components that occupy exactly one column. For each column we keep a list of these
* components.
*/
private transient List[] colComponents;
/**
* Holds the components that occupy exactly one row. For each row we keep a list of these
* components.
*/
private transient List[] rowComponents;
/**
* Caches component minimum and preferred sizes. All requests for component sizes shall be
* directed to the cache.
*/
private final ComponentSizeCache componentSizeCache;
/**
* These functional objects are used to measure component sizes. They abstract from horizontal
* and vertical orientation and so, allow to implement the layout algorithm for both
* orientations with a single set of methods.
*/
private final Measure minimumWidthMeasure;
private final Measure minimumHeightMeasure;
private final Measure preferredWidthMeasure;
private final Measure preferredHeightMeasure;
// Instance Creation ****************************************************
/**
* Constructs an empty FormLayout. Columns and rows must be added before components can be added
* to the layout container.
*
* This constructor is intended to be used in environments that add columns and rows
* dynamically.
*/
public FormLayout() {
this(new ColumnSpec[0], new RowSpec[0]);
}
/**
* Constructs a FormLayout using the given encoded column specifications. The constructed layout
* has no rows; these must be added before components can be added to the layout container. The
* string decoding uses the default LayoutMap.
*
* This constructor is intended to be used with builder classes that add rows dynamically, such
* as the {@code DefaultFormBuilder}
* .
*
* Examples:
* // Label, gap, component
* FormLayout layout = new FormLayout(
* "pref, 4dlu, pref");
*
* // Right-aligned label, gap, component, gap, component
* FormLayout layout = new FormLayout(
* "right:pref, 4dlu, 50dlu, 4dlu, 50dlu");
*
* // Left-aligned labels, gap, components, gap, components
* FormLayout layout = new FormLayout(
* "left:pref, 4dlu, pref, 4dlu, pref");
*
See the class comment for more examples.
*
* @param encodedColumnSpecs comma separated encoded column specifications
*
* @throws NullPointerException if encodedColumnSpecs is {@code null}
*
* @see LayoutMap#getRoot()
*/
public FormLayout(String encodedColumnSpecs) {
this(encodedColumnSpecs, LayoutMap.getRoot());
}
/**
* Constructs a FormLayout using the given encoded column specifications and LayoutMap. The
* constructed layout has no rows; these must be added before components can be added to the
* layout container.
*
* This constructor is intended to be used with builder classes that add rows dynamically, such
* as the {@code DefaultFormBuilder}
* .
*
* Examples:
* // Label, gap, component
* FormLayout layout = new FormLayout(
* "pref, 4dlu, pref",
* myLayoutMap);
*
* // Right-aligned label, gap, component, gap, component
* FormLayout layout = new FormLayout(
* "right:pref, @lcgap, 50dlu, 4dlu, 50dlu",
* myLayoutMap);
*
* // Left-aligned labels, gap, components, gap, components
* FormLayout layout = new FormLayout(
* "left:pref, @lcgap, pref, @myGap, pref",
* myLayoutMap);
*
See the class comment for more examples.
*
* @param encodedColumnSpecs comma separated encoded column specifications
* @param layoutMap expands layout column and row variables
*
* @throws NullPointerException if {@code encodedColumnSpecs} or {@code layoutMap} is
* {@code null}
*
* @see LayoutMap#getRoot()
*
* @since 1.2
*/
public FormLayout(String encodedColumnSpecs, LayoutMap layoutMap) {
this(ColumnSpec.decodeSpecs(encodedColumnSpecs, layoutMap), new RowSpec[0]);
}
/**
* Constructs a FormLayout using the given encoded column and row specifications and the default
* LayoutMap.
*
* This constructor is recommended for most hand-coded layouts.
*
* Examples:
* FormLayout layout = new FormLayout(
* "pref, 4dlu, pref", // columns
* "p, 3dlu, p"); // rows
*
* FormLayout layout = new FormLayout(
* "right:pref, 4dlu, pref", // columns
* "p, 3dlu, p, 3dlu, fill:p:grow"); // rows
*
* FormLayout layout = new FormLayout(
* "left:pref, 4dlu, 50dlu", // columns
* "p, 2px, p, 3dlu, p, 9dlu, p"); // rows
*
* FormLayout layout = new FormLayout(
* "max(75dlu;pref), 4dlu, default", // columns
* "p, 3dlu, p, 3dlu, p, 3dlu, p"); // rows
*
See the class comment for more examples.
*
* @param encodedColumnSpecs comma separated encoded column specifications
* @param encodedRowSpecs comma separated encoded row specifications
*
* @throws NullPointerException if encodedColumnSpecs or encodedRowSpecs is {@code null}
*
* @see LayoutMap#getRoot()
*/
public FormLayout(String encodedColumnSpecs, String encodedRowSpecs) {
this(encodedColumnSpecs, encodedRowSpecs, LayoutMap.getRoot());
}
/**
* Constructs a FormLayout using the given encoded column and row specifications and the given
* LayoutMap.
*
* Examples:
* FormLayout layout = new FormLayout(
* "pref, 4dlu, pref", // columns
* "p, 3dlu, p", // rows
* myLayoutMap); // custom LayoutMap
*
* FormLayout layout = new FormLayout(
* "right:pref, 4dlu, pref", // columns
* "p, @lgap, p, @lgap, fill:p:grow",// rows
* myLayoutMap); // custom LayoutMap
*
* FormLayout layout = new FormLayout(
* "left:pref, 4dlu, 50dlu", // columns
* "p, 2px, p, 3dlu, p, 9dlu, p", // rows
* myLayoutMap); // custom LayoutMap
*
* FormLayout layout = new FormLayout(
* "max(75dlu;pref), 4dlu, default", // columns
* "p, 3dlu, p, 3dlu, p, 3dlu, p", // rows
* myLayoutMap); // custom LayoutMap
*
See the class comment for more examples.
*
* @param encodedColumnSpecs comma separated encoded column specifications
* @param encodedRowSpecs comma separated encoded row specifications
* @param layoutMap expands layout column and row variables
*
* @throws NullPointerException if {@code encodedColumnSpecs}, {@code encodedRowSpecs}, or
* {@code layoutMap} is {@code null}
*
* @since 1.2
*/
public FormLayout(
String encodedColumnSpecs,
String encodedRowSpecs,
LayoutMap layoutMap) {
this(ColumnSpec.decodeSpecs(encodedColumnSpecs, layoutMap),
RowSpec.decodeSpecs(encodedRowSpecs, layoutMap));
}
/**
* Constructs a FormLayout using the given column specifications. The constructed layout has no
* rows; these must be added before components can be added to the layout container.
*
* @param colSpecs an array of column specifications.
* @throws NullPointerException if {@code colSpecs} is {@code null}
*
* @since 1.1
*/
public FormLayout(ColumnSpec[] colSpecs) {
this(colSpecs, new RowSpec[]{});
}
/**
* Constructs a FormLayout using the given column and row specifications.
*
* @param colSpecs an array of column specifications.
* @param rowSpecs an array of row specifications.
* @throws NullPointerException if {@code colSpecs} or {@code rowSpecs} is {@code null}
*/
public FormLayout(ColumnSpec[] colSpecs, RowSpec[] rowSpecs) {
checkNotNull(colSpecs, "The column specifications must not be null.");
checkNotNull(rowSpecs, "The row specifications must not be null.");
this.colSpecs = new ArrayList(Arrays.asList(colSpecs));
this.rowSpecs = new ArrayList(Arrays.asList(rowSpecs));
colGroupIndices = new int[][]{};
rowGroupIndices = new int[][]{};
int initialCapacity = colSpecs.length * rowSpecs.length / 4;
constraintMap = new HashMap(initialCapacity);
componentSizeCache = new ComponentSizeCache(initialCapacity);
minimumWidthMeasure = new MinimumWidthMeasure(componentSizeCache);
minimumHeightMeasure = new MinimumHeightMeasure(componentSizeCache);
preferredWidthMeasure = new PreferredWidthMeasure(componentSizeCache);
preferredHeightMeasure = new PreferredHeightMeasure(componentSizeCache);
}
// Accessing the Column and Row Specifications **************************
/**
* Returns the number of columns in this layout.
*
* @return the number of columns
*/
public int getColumnCount() {
return colSpecs.size();
}
/**
* Returns the {@code ColumnSpec} at the specified column index.
*
* @param columnIndex the column index of the requested {@code ColumnSpec}
* @return the {@code ColumnSpec} at the specified column
* @throws IndexOutOfBoundsException if the column index is out of range
*/
public ColumnSpec getColumnSpec(int columnIndex) {
return colSpecs.get(columnIndex - 1);
}
/**
* Sets the ColumnSpec at the specified column index.
*
* @param columnIndex the index of the column to be changed
* @param columnSpec the ColumnSpec to be set
* @throws NullPointerException if {@code columnSpec} is {@code null}
* @throws IndexOutOfBoundsException if the column index is out of range
*/
public void setColumnSpec(int columnIndex, ColumnSpec columnSpec) {
checkNotNull(columnSpec, "The column spec must not be null.");
colSpecs.set(columnIndex - 1, columnSpec);
}
/**
* Appends the given column specification to the right hand side of all columns.
*
* @param columnSpec the column specification to be added
* @throws NullPointerException if {@code columnSpec} is {@code null}
*/
public void appendColumn(ColumnSpec columnSpec) {
checkNotNull(columnSpec, "The column spec must not be null.");
colSpecs.add(columnSpec);
}
/**
* Inserts the specified column at the specified position. Shifts components that intersect the
* new column to the right hand side and readjusts column groups.
*
* The component shift works as follows: components that were located on the right hand side of
* the inserted column are shifted one column to the right; component column span is increased
* by one if it intersects the new column.
*
* Column group indices that are greater or equal than the given column index will be increased
* by one.
*
* @param columnIndex index of the column to be inserted
* @param columnSpec specification of the column to be inserted
* @throws IndexOutOfBoundsException if the column index is out of range
*/
public void insertColumn(int columnIndex, ColumnSpec columnSpec) {
if (columnIndex < 1 || columnIndex > getColumnCount()) {
throw new IndexOutOfBoundsException(
"The column index " + columnIndex
+ "must be in the range [1, " + getColumnCount() + "].");
}
colSpecs.add(columnIndex - 1, columnSpec);
shiftComponentsHorizontally(columnIndex, false);
adjustGroupIndices(colGroupIndices, columnIndex, false);
}
/**
* Removes the column with the given column index from the layout. Components will be rearranged
* and column groups will be readjusted. Therefore, the column must not contain components and
* must not be part of a column group.
*
* The component shift works as follows: components that were located on the right hand side of
* the removed column are moved one column to the left; component column span is decreased by
* one if it intersects the removed column.
*
* Column group indices that are greater than the column index will be decreased by one.
*
* Note: If one of the constraints mentioned above is violated, this layout's
* state becomes illegal and it is unsafe to work with this layout. A typical layout
* implementation can ensure that these constraints are not violated. However, in some cases you
* may need to check these conditions before you invoke this method. The Forms extras contain
* source code for class {@code FormLayoutUtils} that provides the required test methods:
* {@code #columnContainsComponents(Container, int)} and
* {@code #isGroupedColumn(FormLayout, int)}.
*
* @param columnIndex index of the column to remove
* @throws IndexOutOfBoundsException if the column index is out of range
* @throws IllegalStateException if the column contains components or if the column is already
* grouped
*
* see com.jgoodies.forms.extras.FormLayoutUtils#columnContainsComponent(Container, int) see
* com.jgoodies.forms.extras.FormLayoutUtils#isGroupedColumn(FormLayout, int)
*/
public void removeColumn(int columnIndex) {
if (columnIndex < 1 || columnIndex > getColumnCount()) {
throw new IndexOutOfBoundsException(
"The column index " + columnIndex
+ " must be in the range [1, " + getColumnCount() + "].");
}
colSpecs.remove(columnIndex - 1);
shiftComponentsHorizontally(columnIndex, true);
adjustGroupIndices(colGroupIndices, columnIndex, true);
}
/**
* Returns the number of rows in this layout.
*
* @return the number of rows
*/
public int getRowCount() {
return rowSpecs.size();
}
/**
* Returns the {@code RowSpec} at the specified row index.
*
* @param rowIndex the row index of the requested {@code RowSpec}
* @return the {@code RowSpec} at the specified row
* @throws IndexOutOfBoundsException if the row index is out of range
*/
public RowSpec getRowSpec(int rowIndex) {
return rowSpecs.get(rowIndex - 1);
}
/**
* Sets the RowSpec at the specified row index.
*
* @param rowIndex the index of the row to be changed
* @param rowSpec the RowSpec to be set
* @throws NullPointerException if {@code rowSpec} is {@code null}
* @throws IndexOutOfBoundsException if the row index is out of range
*/
public void setRowSpec(int rowIndex, RowSpec rowSpec) {
checkNotNull(rowSpec, "The row spec must not be null.");
rowSpecs.set(rowIndex - 1, rowSpec);
}
/**
* Appends the given row specification to the bottom of all rows.
*
* @param rowSpec the row specification to be added to the form layout
* @throws NullPointerException if {@code rowSpec} is {@code null}
*/
public void appendRow(RowSpec rowSpec) {
checkNotNull(rowSpec, "The row spec must not be null.");
rowSpecs.add(rowSpec);
}
/**
* Inserts the specified column at the specified position. Shifts components that intersect the
* new column to the right and readjusts column groups.
*
* The component shift works as follows: components that were located on the right hand side of
* the inserted column are shifted one column to the right; component column span is increased
* by one if it intersects the new column.
*
* Column group indices that are greater or equal than the given column index will be increased
* by one.
*
* @param rowIndex index of the row to be inserted
* @param rowSpec specification of the row to be inserted
* @throws IndexOutOfBoundsException if the row index is out of range
*/
public void insertRow(int rowIndex, RowSpec rowSpec) {
if (rowIndex < 1 || rowIndex > getRowCount()) {
throw new IndexOutOfBoundsException(
"The row index " + rowIndex
+ " must be in the range [1, " + getRowCount() + "].");
}
rowSpecs.add(rowIndex - 1, rowSpec);
shiftComponentsVertically(rowIndex, false);
adjustGroupIndices(rowGroupIndices, rowIndex, false);
}
/**
* Removes the row with the given row index from the layout. Components will be rearranged and
* row groups will be readjusted. Therefore, the row must not contain components and must not be
* part of a row group.
*
* The component shift works as follows: components that were located below the removed row are
* moved up one row; component row span is decreased by one if it intersects the removed row.
*
* Row group indices that are greater than the row index will be decreased by one.
*
* Note: If one of the constraints mentioned above is violated, this layout's
* state becomes illegal and it is unsafe to work with this layout. A typical layout
* implementation can ensure that these constraints are not violated. However, in some cases you
* may need to check these conditions before you invoke this method. The Forms extras contain
* source code for class {@code FormLayoutUtils} that provides the required test methods:
* {@code #rowContainsComponents(Container, int)} and
* {@code #isGroupedRow(FormLayout, int)}.
*
* @param rowIndex index of the row to remove
* @throws IndexOutOfBoundsException if the row index is out of range
* @throws IllegalStateException if the row contains components or if the row is already grouped
*
* see com.jgoodies.forms.extras.FormLayoutUtils#rowContainsComponent(Container, int) see
* com.jgoodies.forms.extras.FormLayoutUtils#isGroupedRow(FormLayout, int)
*/
public void removeRow(int rowIndex) {
if (rowIndex < 1 || rowIndex > getRowCount()) {
throw new IndexOutOfBoundsException(
"The row index " + rowIndex
+ "must be in the range [1, " + getRowCount() + "].");
}
rowSpecs.remove(rowIndex - 1);
shiftComponentsVertically(rowIndex, true);
adjustGroupIndices(rowGroupIndices, rowIndex, true);
}
/**
* Shifts components horizontally, either to the right if a column has been inserted or to the
* left if a column has been removed.
*
* @param columnIndex index of the column to remove
* @param remove true for remove, false for insert
* @throws IllegalStateException if a removed column contains components
*/
private void shiftComponentsHorizontally(int columnIndex, boolean remove) {
final int offset = remove ? -1 : 1;
for (Object element : constraintMap.entrySet()) {
Map.Entry entry = (Map.Entry) element;
CellConstraints constraints = (CellConstraints) entry.getValue();
int x1 = constraints.gridX;
int w = constraints.gridWidth;
int x2 = x1 + w - 1;
if (x1 == columnIndex && remove) {
throw new IllegalStateException(
"The removed column " + columnIndex
+ " must not contain component origins.\n"
+ "Illegal component=" + entry.getKey());
} else if (x1 >= columnIndex) {
constraints.gridX += offset;
} else if (x2 >= columnIndex) {
constraints.gridWidth += offset;
}
}
}
/**
* Shifts components vertically, either to the bottom if a row has been inserted or to the top
* if a row has been removed.
*
* @param rowIndex index of the row to remove
* @param remove true for remove, false for insert
* @throws IllegalStateException if a removed column contains components
*/
private void shiftComponentsVertically(int rowIndex, boolean remove) {
final int offset = remove ? -1 : 1;
for (Object element : constraintMap.entrySet()) {
Map.Entry entry = (Map.Entry) element;
CellConstraints constraints = (CellConstraints) entry.getValue();
int y1 = constraints.gridY;
int h = constraints.gridHeight;
int y2 = y1 + h - 1;
if (y1 == rowIndex && remove) {
throw new IllegalStateException(
"The removed row " + rowIndex
+ " must not contain component origins.\n"
+ "Illegal component=" + entry.getKey());
} else if (y1 >= rowIndex) {
constraints.gridY += offset;
} else if (y2 >= rowIndex) {
constraints.gridHeight += offset;
}
}
}
/**
* Adjusts group indices. Shifts the given groups to left, right, up, down according to the
* specified remove or add flag.
*
* @param allGroupIndices the groups to be adjusted
* @param modifiedIndex the modified column or row index
* @param remove true for remove, false for add
* @throws IllegalStateException if we remove and the index is grouped
*/
private static void adjustGroupIndices(int[][] allGroupIndices,
int modifiedIndex, boolean remove) {
final int offset = remove ? -1 : +1;
for (int[] allGroupIndice : allGroupIndices) {
int[] groupIndices = allGroupIndice;
for (int i = 0; i < groupIndices.length; i++) {
int index = groupIndices[i];
if (index == modifiedIndex && remove) {
throw new IllegalStateException(
"The removed index " + modifiedIndex + " must not be grouped.");
} else if (index >= modifiedIndex) {
groupIndices[i] += offset;
}
}
}
}
// Accessing Constraints ************************************************
/**
* Looks up and returns the constraints for the specified component. A copy of the
* actualCellConstraints object is returned.
*
* @param component the component to be queried
* @return the CellConstraints for the specified component
* @throws NullPointerException if {@code component} is {@code null}
* @throws IllegalStateException if {@code component} has not been added to the container
*/
public CellConstraints getConstraints(Component component) {
return (CellConstraints) getConstraints0(component).clone();
}
private CellConstraints getConstraints0(Component component) {
checkNotNull(component, "The component must not be null.");
CellConstraints constraints = constraintMap.get(component);
checkState(constraints != null, "The component has not been added to the container.");
return constraints;
}
/**
* Sets the constraints for the specified component in this layout.
*
* @param component the component to be modified
* @param constraints the constraints to be applied
* @throws NullPointerException if {@code component} or {@code constraints} is {@code null}
*/
public void setConstraints(Component component, CellConstraints constraints) {
checkNotNull(component, "The component must not be null.");
checkNotNull(constraints, "The constraints must not be null.");
constraints.ensureValidGridBounds(getColumnCount(), getRowCount());
constraintMap.put(component, (CellConstraints) constraints.clone());
}
/**
* Removes the constraints for the specified component in this layout.
*
* @param component the component to be modified
*/
private void removeConstraints(Component component) {
constraintMap.remove(component);
componentSizeCache.removeEntry(component);
}
// Accessing Column and Row Groups **************************************
/**
* Returns a deep copy of the column groups.
*
* @return the column groups as two-dimensional int array
*/
public int[][] getColumnGroups() {
return deepClone(colGroupIndices);
}
/**
* Sets the column groups, where each column in a group gets the same group wide width. Each
* group is described by an array of integers that are interpreted as column indices. The
* parameter is an array of such group descriptions.
*
* Examples:
* // Group columns 1, 3 and 4.
* setColumnGroups(new int[][]{ {1, 3, 4}});
*
* // Group columns 1, 3, 4, and group columns 7 and 9
* setColumnGroups(new int[][]{ {1, 3, 4}, {7, 9}});
*
*
* @param colGroupIndices a two-dimensional array of column groups indices
* @throws IndexOutOfBoundsException if an index is outside the grid
* @throws IllegalArgumentException if a column index is used twice
*/
public void setColumnGroups(int[][] colGroupIndices) {
int maxColumn = getColumnCount();
boolean[] usedIndices = new boolean[maxColumn + 1];
for (int group = 0; group < colGroupIndices.length; group++) {
for (int j = 0; j < colGroupIndices[group].length; j++) {
int colIndex = colGroupIndices[group][j];
if (colIndex < 1 || colIndex > maxColumn) {
throw new IndexOutOfBoundsException(
"Invalid column group index " + colIndex
+ " in group " + (group + 1));
}
if (usedIndices[colIndex]) {
throw new IllegalArgumentException(
"Column index " + colIndex + " must not be used in multiple column groups.");
}
usedIndices[colIndex] = true;
}
}
this.colGroupIndices = deepClone(colGroupIndices);
}
/**
* Adds the specified column index to the last column group. In case there are no groups, a new
* group will be created.
*
* @param columnIndex the column index to be set grouped
*/
public void addGroupedColumn(int columnIndex) {
int[][] newColGroups = getColumnGroups();
// Create a group if none exists.
if (newColGroups.length == 0) {
newColGroups = new int[][]{{columnIndex}};
} else {
int lastGroupIndex = newColGroups.length - 1;
int[] lastGroup = newColGroups[lastGroupIndex];
int groupSize = lastGroup.length;
int[] newLastGroup = new int[groupSize + 1];
System.arraycopy(lastGroup, 0, newLastGroup, 0, groupSize);
newLastGroup[groupSize] = columnIndex;
newColGroups[lastGroupIndex] = newLastGroup;
}
setColumnGroups(newColGroups);
}
/**
* Returns a deep copy of the row groups.
*
* @return the row groups as two-dimensional int array
*/
public int[][] getRowGroups() {
return deepClone(rowGroupIndices);
}
/**
* Sets the row groups, where each row in such a group gets the same group wide height. Each
* group is described by an array of integers that are interpreted as row indices. The parameter
* is an array of such group descriptions.
*
* Examples:
* // Group rows 1 and 2.
* setRowGroups(new int[][]{ {1, 2}});
*
* // Group rows 1 and 2, and group rows 5, 7, and 9.
* setRowGroups(new int[][]{ {1, 2}, {5, 7, 9}});
*
*
* @param rowGroupIndices a two-dimensional array of row group indices.
* @throws IndexOutOfBoundsException if an index is outside the grid
*/
public void setRowGroups(int[][] rowGroupIndices) {
int rowCount = getRowCount();
boolean[] usedIndices = new boolean[rowCount + 1];
for (int i = 0; i < rowGroupIndices.length; i++) {
for (int j = 0; j < rowGroupIndices[i].length; j++) {
int rowIndex = rowGroupIndices[i][j];
if (rowIndex < 1 || rowIndex > rowCount) {
throw new IndexOutOfBoundsException(
"Invalid row group index " + rowIndex
+ " in group " + (i + 1));
}
if (usedIndices[rowIndex]) {
throw new IllegalArgumentException(
"Row index " + rowIndex + " must not be used in multiple row groups.");
}
usedIndices[rowIndex] = true;
}
}
this.rowGroupIndices = deepClone(rowGroupIndices);
}
/**
* Adds the specified row index to the last row group. In case there are no groups, a new group
* will be created.
*
* @param rowIndex the index of the row that should be grouped
*/
public void addGroupedRow(int rowIndex) {
int[][] newRowGroups = getRowGroups();
// Create a group if none exists.
if (newRowGroups.length == 0) {
newRowGroups = new int[][]{{rowIndex}};
} else {
int lastGroupIndex = newRowGroups.length - 1;
int[] lastGroup = newRowGroups[lastGroupIndex];
int groupSize = lastGroup.length;
int[] newLastGroup = new int[groupSize + 1];
System.arraycopy(lastGroup, 0, newLastGroup, 0, groupSize);
newLastGroup[groupSize] = rowIndex;
newRowGroups[lastGroupIndex] = newLastGroup;
}
setRowGroups(newRowGroups);
}
// Other Accessors ********************************************************
/**
* Returns whether invisible components shall be taken into account by this layout. This
* container-wide setting can be overridden per component. See
* {@link #setHonorsVisibility(boolean)} for details.
*
* @return {@code true} if the component visibility is honored by this FormLayout, {@code false}
* if it is ignored. This setting can be overridden for individual CellConstraints using
* {@link #setHonorsVisibility(Component, Boolean)}.
*
* @since 1.2
*/
public boolean getHonorsVisibility() {
return honorsVisibility;
}
/**
* Specifies whether invisible components shall be taken into account by this layout for
* computing the layout size and setting component bounds. If set to {@code true} invisible
* components will be ignored by the layout. If set to {@code false} components will be taken
* into account regardless of their visibility. Visible components are always used for sizing
* and positioning.
*
* The default value for this setting is {@code true}. It is useful to set the value to
* {@code false} (in other words to ignore the visibility) if you switch the component
* visibility dynamically and want the container to retain the size and component positions.
*
* This container-wide default setting can be overridden per component using
* {@link #setHonorsVisibility(Component, Boolean)}
* .
*
* Components are taken into account, if
* - they are visible, or
*
- they have no individual setting and the container-wide settings ignores the visibility
* (honorsVisibility set to {@code false}), or
*
- the individual component ignores the visibility.
*
*
* @param b {@code true} to honor the visibility, i.e. to exclude invisible components from the
* sizing and positioning, {@code false} to ignore the visibility, in other words to layout
* visible and invisible components
*
* @since 1.2
*/
public void setHonorsVisibility(boolean b) {
boolean oldHonorsVisibility = getHonorsVisibility();
if (oldHonorsVisibility == b) {
return;
}
honorsVisibility = b;
Set componentSet = constraintMap.keySet();
if (componentSet.isEmpty()) {
return;
}
Component firstComponent = (Component) componentSet.iterator().next();
Container container = firstComponent.getParent();
invalidateAndRepaint(container);
}
/**
* Specifies whether the given component shall be taken into account for sizing and positioning.
* This setting overrides the container-wide default. See {@link #setHonorsVisibility(boolean)}
* for details.
*
* @param component the component that shall get an individual setting
* @param b {@code Boolean.TRUE} to override the container default and honor the visibility for
* the given component, {@code Boolean.FALSE} to override the container default and ignore the
* visibility for the given component, {@code null} to use the container default value as
* specified by {@link #getHonorsVisibility()}.
*
* @since 1.2
*/
public void setHonorsVisibility(Component component, Boolean b) {
CellConstraints constraints = getConstraints0(component);
if (Objects.equals(b, constraints.honorsVisibility)) {
return;
}
constraints.honorsVisibility = b;
invalidateAndRepaint(component.getParent());
}
// Implementing the LayoutManager and LayoutManager2 Interfaces *********
/**
* Throws an {@code UnsupportedOperationException}. Does not add the specified component with
* the specified name to the layout.
*
* @param name indicates entry's position and anchor
* @param component component to add
* @throws UnsupportedOperationException always
*/
@Override
public void addLayoutComponent(String name, Component component) {
throw new UnsupportedOperationException(
"Use #addLayoutComponent(Component, Object) instead.");
}
/**
* Adds the specified component to the layout, using the specified {@code constraints} object.
* Note that constraints are mutable and are, therefore, cloned when cached.
*
* @param comp the component to be added
* @param constraints the component's cell constraints
* @throws NullPointerException if {@code constraints} is {@code null}
* @throws IllegalArgumentException if {@code constraints} is neither a String, nor a
* CellConstraints object, or a String that is rejected by the CellConstraints construction
*/
@Override
public void addLayoutComponent(Component comp, Object constraints) {
checkNotNull(constraints, "The constraints must not be null.");
if (constraints instanceof String) {
setConstraints(comp, new CellConstraints((String) constraints));
} else if (constraints instanceof CellConstraints) {
setConstraints(comp, (CellConstraints) constraints);
} else {
throw new IllegalArgumentException("Illegal constraint type " + constraints.getClass());
}
}
/**
* Removes the specified component from this layout.
*
* Most applications do not call this method directly.
*
* @param comp the component to be removed.
* @see Container#remove(java.awt.Component)
* @see Container#removeAll()
*/
@Override
public void removeLayoutComponent(Component comp) {
removeConstraints(comp);
}
// Layout Requests ******************************************************
/**
* Determines the minimum size of the {@code parent} container using this form layout.
*
* Most applications do not call this method directly.
*
* @param parent the container in which to do the layout
* @return the minimum size of the {@code parent} container
*
* @see Container#doLayout()
*/
@Override
public Dimension minimumLayoutSize(Container parent) {
return computeLayoutSize(parent,
minimumWidthMeasure,
minimumHeightMeasure);
}
/**
* Determines the preferred size of the {@code parent} container using this form layout.
*
* Most applications do not call this method directly.
*
* @param parent the container in which to do the layout
* @return the preferred size of the {@code parent} container
*
* @see Container#getPreferredSize()
*/
@Override
public Dimension preferredLayoutSize(Container parent) {
return computeLayoutSize(parent,
preferredWidthMeasure,
preferredHeightMeasure);
}
/**
* Returns the maximum dimensions for this layout given the components in the specified target
* container.
*
* @param target the container which needs to be laid out
* @see Container
* @see #minimumLayoutSize(Container)
* @see #preferredLayoutSize(Container)
* @return the maximum dimensions for this layout
*/
@Override
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
/**
* Returns the alignment along the x axis. This specifies how the component would like to be
* aligned relative to other components. The value should be a number between 0 and 1 where 0
* represents alignment along the origin, 1 is aligned the farthest away from the origin, 0.5 is
* centered, etc.
*
* @param parent the parent container
* @return the value {@code 0.5f} to indicate center alignment
*/
@Override
public float getLayoutAlignmentX(Container parent) {
return 0.5f;
}
/**
* Returns the alignment along the y axis. This specifies how the component would like to be
* aligned relative to other components. The value should be a number between 0 and 1 where 0
* represents alignment along the origin, 1 is aligned the farthest away from the origin, 0.5 is
* centered, etc.
*
* @param parent the parent container
* @return the value {@code 0.5f} to indicate center alignment
*/
@Override
public float getLayoutAlignmentY(Container parent) {
return 0.5f;
}
/**
* Invalidates the layout, indicating that if the layout manager has cached information it
* should be discarded.
*
* @param target the container that holds the layout to be invalidated
*/
@Override
public void invalidateLayout(Container target) {
invalidateCaches();
}
/**
* Lays out the specified container using this form layout. This method reshapes components in
* the specified container in order to satisfy the constraints of this {@code FormLayout}
* object.
*
* Most applications do not call this method directly.
*
* The form layout performs the following steps:
*
* - find components that occupy exactly one column or row
*
- compute minimum widths and heights
*
- compute preferred widths and heights
*
- give cols and row equal size if they share a group
*
- compress default columns and rows if total is less than pref size
*
- give cols and row equal size if they share a group
*
- distribute free space
*
- set components bounds
*
*
* @param parent the container in which to do the layout
* @see Container
* @see Container#doLayout()
*/
@Override
public void layoutContainer(Container parent) {
synchronized (parent.getTreeLock()) {
initializeColAndRowComponentLists();
Dimension size = parent.getSize();
Insets insets = parent.getInsets();
int totalWidth = size.width - insets.left - insets.right;
int totalHeight = size.height - insets.top - insets.bottom;
int[] x = computeGridOrigins(parent,
totalWidth, insets.left,
colSpecs,
colComponents,
colGroupIndices,
minimumWidthMeasure,
preferredWidthMeasure
);
int[] y = computeGridOrigins(parent,
totalHeight, insets.top,
rowSpecs,
rowComponents,
rowGroupIndices,
minimumHeightMeasure,
preferredHeightMeasure
);
layoutComponents(x, y);
}
}
// Layout Algorithm *****************************************************
/**
* Initializes two lists for columns and rows that hold a column's or row's components that span
* only this column or row.
*
* Iterates over all components and their associated constraints; every component that has a
* column span or row span of 1 is put into the column's or row's component list.
*/
private void initializeColAndRowComponentLists() {
colComponents = new List[getColumnCount()];
for (int i = 0; i < getColumnCount(); i++) {
colComponents[i] = new ArrayList();
}
rowComponents = new List[getRowCount()];
for (int i = 0; i < getRowCount(); i++) {
rowComponents[i] = new ArrayList();
}
for (Object element : constraintMap.entrySet()) {
Map.Entry entry = (Map.Entry) element;
Component component = (Component) entry.getKey();
CellConstraints constraints = (CellConstraints) entry.getValue();
if (takeIntoAccount(component, constraints)) {
if (constraints.gridWidth == 1) {
colComponents[constraints.gridX - 1].add(component);
}
if (constraints.gridHeight == 1) {
rowComponents[constraints.gridY - 1].add(component);
}
}
}
}
/**
* Computes and returns the layout size of the given {@code parent} container using the
* specified measures.
*
* @param parent the container in which to do the layout
* @param defaultWidthMeasure the measure used to compute the default width
* @param defaultHeightMeasure the measure used to compute the default height
* @return the layout size of the {@code parent} container
*/
private Dimension computeLayoutSize(Container parent,
Measure defaultWidthMeasure,
Measure defaultHeightMeasure) {
synchronized (parent.getTreeLock()) {
initializeColAndRowComponentLists();
int[] colWidths = maximumSizes(parent, colSpecs, colComponents,
minimumWidthMeasure,
preferredWidthMeasure,
defaultWidthMeasure);
int[] rowHeights = maximumSizes(parent, rowSpecs, rowComponents,
minimumHeightMeasure,
preferredHeightMeasure,
defaultHeightMeasure);
int[] groupedWidths = groupedSizes(colGroupIndices, colWidths);
int[] groupedHeights = groupedSizes(rowGroupIndices, rowHeights);
// Convert sizes to origins.
int[] xOrigins = computeOrigins(groupedWidths, 0);
int[] yOrigins = computeOrigins(groupedHeights, 0);
int width1 = sum(groupedWidths);
int height1 = sum(groupedHeights);
int maxWidth = width1;
int maxHeight = height1;
/*
* Take components that span multiple columns or rows into account.
* This shall be done if and only if a component spans an interval
* that can grow.
*/
// First computes the maximum number of cols/rows a component
// can span without spanning a growing column.
int[] maxFixedSizeColsTable = computeMaximumFixedSpanTable(colSpecs);
int[] maxFixedSizeRowsTable = computeMaximumFixedSpanTable(rowSpecs);
for (Object element : constraintMap.entrySet()) {
Map.Entry entry = (Map.Entry) element;
Component component = (Component) entry.getKey();
CellConstraints constraints = (CellConstraints) entry.getValue();
if (!takeIntoAccount(component, constraints)) {
continue;
}
if (constraints.gridWidth > 1
&& constraints.gridWidth > maxFixedSizeColsTable[constraints.gridX - 1]) {
//int compWidth = minimumWidthMeasure.sizeOf(component);
int compWidth = defaultWidthMeasure.sizeOf(component);
//int compWidth = preferredWidthMeasure.sizeOf(component);
int gridX1 = constraints.gridX - 1;
int gridX2 = gridX1 + constraints.gridWidth;
int lead = xOrigins[gridX1];
int trail = width1 - xOrigins[gridX2];
int myWidth = lead + compWidth + trail;
if (myWidth > maxWidth) {
maxWidth = myWidth;
}
}
if (constraints.gridHeight > 1
&& constraints.gridHeight > maxFixedSizeRowsTable[constraints.gridY - 1]) {
//int compHeight = minimumHeightMeasure.sizeOf(component);
int compHeight = defaultHeightMeasure.sizeOf(component);
//int compHeight = preferredHeightMeasure.sizeOf(component);
int gridY1 = constraints.gridY - 1;
int gridY2 = gridY1 + constraints.gridHeight;
int lead = yOrigins[gridY1];
int trail = height1 - yOrigins[gridY2];
int myHeight = lead + compHeight + trail;
if (myHeight > maxHeight) {
maxHeight = myHeight;
}
}
}
Insets insets = parent.getInsets();
int width = maxWidth + insets.left + insets.right;
int height = maxHeight + insets.top + insets.bottom;
return new Dimension(width, height);
}
}
/**
* Computes and returns the grid's origins.
*
* @param container the layout container
* @param totalSize the total size to assign
* @param offset the offset from left or top margin
* @param formSpecs the column or row specs, resp.
* @param componentLists the components list for each col/row
* @param minMeasure the measure used to determine min sizes
* @param prefMeasure the measure used to determine pre sizes
* @param groupIndices the group specification
* @return an int array with the origins
*/
private static int[] computeGridOrigins(Container container,
int totalSize, int offset,
List formSpecs,
List[] componentLists,
int[][] groupIndices,
Measure minMeasure,
Measure prefMeasure) {
/* For each spec compute the minimum and preferred size that is
* the maximum of all component minimum and preferred sizes resp.
*/
int[] minSizes = maximumSizes(container, formSpecs, componentLists,
minMeasure, prefMeasure, minMeasure);
int[] prefSizes = maximumSizes(container, formSpecs, componentLists,
minMeasure, prefMeasure, prefMeasure);
int[] groupedMinSizes = groupedSizes(groupIndices, minSizes);
int[] groupedPrefSizes = groupedSizes(groupIndices, prefSizes);
int totalMinSize = sum(groupedMinSizes);
int totalPrefSize = sum(groupedPrefSizes);
int[] compressedSizes = compressedSizes(formSpecs,
totalSize,
totalMinSize,
totalPrefSize,
groupedMinSizes,
prefSizes);
int[] groupedSizes = groupedSizes(groupIndices, compressedSizes);
int totalGroupedSize = sum(groupedSizes);
int[] sizes = distributedSizes(formSpecs,
totalSize,
totalGroupedSize,
groupedSizes);
return computeOrigins(sizes, offset);
}
/**
* Computes origins from sizes taking the specified offset into account.
*
* @param sizes the array of sizes
* @param offset an offset for the first origin
* @return an array of origins
*/
private static int[] computeOrigins(int[] sizes, int offset) {
int count = sizes.length;
int[] origins = new int[count + 1];
origins[0] = offset;
for (int i = 1; i <= count; i++) {
origins[i] = origins[i - 1] + sizes[i - 1];
}
return origins;
}
/**
* Lays out the components using the given x and y origins, the column and row specifications,
* and the component constraints.
*
* The actual computation is done by each component's form constraint object. We just compute
* the cell, the cell bounds and then hand over the component, cell bounds, and measure to the
* form constraints. This will allow potential subclasses of {@code CellConstraints} to do
* special micro-layout corrections. For example, such a subclass could map JComponent classes
* to visual layout bounds that may lead to a slightly different bounds.
*
* @param x an int array of the horizontal origins
* @param y an int array of the vertical origins
*/
private void layoutComponents(int[] x, int[] y) {
Rectangle cellBounds = new Rectangle();
for (Object element : constraintMap.entrySet()) {
Map.Entry entry = (Map.Entry) element;
Component component = (Component) entry.getKey();
CellConstraints constraints = (CellConstraints) entry.getValue();
int gridX = constraints.gridX - 1;
int gridY = constraints.gridY - 1;
int gridWidth = constraints.gridWidth;
int gridHeight = constraints.gridHeight;
cellBounds.x = x[gridX];
cellBounds.y = y[gridY];
cellBounds.width = x[gridX + gridWidth] - cellBounds.x;
cellBounds.height = y[gridY + gridHeight] - cellBounds.y;
constraints.setBounds(component, this, cellBounds,
minimumWidthMeasure, minimumHeightMeasure,
preferredWidthMeasure, preferredHeightMeasure);
}
}
/**
* Invalidates the component size caches.
*/
private void invalidateCaches() {
componentSizeCache.invalidate();
}
/**
* Computes and returns the sizes for the given form specs, component lists and measures for
* minimum, preferred, and default size.
*
* @param container the layout container
* @param formSpecs the column or row specs, resp.
* @param componentLists the components list for each col/row
* @param minMeasure the measure used to determine min sizes
* @param prefMeasure the measure used to determine pre sizes
* @param defaultMeasure the measure used to determine default sizes
* @return the column or row sizes
*/
private static int[] maximumSizes(Container container,
List formSpecs,
List[] componentLists,
Measure minMeasure,
Measure prefMeasure,
Measure defaultMeasure) {
FormSpec formSpec;
int size = formSpecs.size();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
formSpec = (FormSpec) formSpecs.get(i);
result[i] = formSpec.maximumSize(container,
componentLists[i],
minMeasure,
prefMeasure,
defaultMeasure);
}
return result;
}
/**
* Computes and returns the compressed sizes. Compresses space for columns and rows iff the
* available space is less than the total preferred size but more than the total minimum
* size.
*
* Only columns and rows that are specified to be compressible will be affected. You can specify
* a column and row as compressible by giving it the component size default.
*
* @param formSpecs the column or row specs to use
* @param totalSize the total available size
* @param totalMinSize the sum of all minimum sizes
* @param totalPrefSize the sum of all preferred sizes
* @param minSizes an int array of column/row minimum sizes
* @param prefSizes an int array of column/row preferred sizes
* @return an int array of compressed column/row sizes
*/
private static int[] compressedSizes(List formSpecs,
int totalSize, int totalMinSize, int totalPrefSize,
int[] minSizes, int[] prefSizes) {
// If we have less space than the total min size, answer the min sizes.
if (totalSize < totalMinSize) {
return minSizes;
}
// If we have more space than the total pref size, answer the pref sizes.
if (totalSize >= totalPrefSize) {
return prefSizes;
}
int count = formSpecs.size();
int[] sizes = new int[count];
double totalCompressionSpace = totalPrefSize - totalSize;
double maxCompressionSpace = totalPrefSize - totalMinSize;
double compressionFactor = totalCompressionSpace / maxCompressionSpace;
// System.out.println("Total compression space=" + totalCompressionSpace);
// System.out.println("Max compression space =" + maxCompressionSpace);
// System.out.println("Compression factor =" + compressionFactor);
for (int i = 0; i < count; i++) {
FormSpec formSpec = (FormSpec) formSpecs.get(i);
sizes[i] = prefSizes[i];
if (formSpec.getSize().compressible()) {
sizes[i] -= (int) Math.round((prefSizes[i] - minSizes[i])
* compressionFactor);
}
}
return sizes;
}
/**
* Computes and returns the grouped sizes. Gives grouped columns and rows the same size.
*
* @param groups the group specification
* @param rawSizes the raw sizes before the grouping
* @return the grouped sizes
*/
private static int[] groupedSizes(int[][] groups, int[] rawSizes) {
// Return the compressed sizes if there are no groups.
if (groups == null || groups.length == 0) {
return rawSizes;
}
// Initialize the result with the given compressed sizes.
int[] sizes = new int[rawSizes.length];
for (int i = 0; i < sizes.length; i++) {
sizes[i] = rawSizes[i];
}
// For each group equalize the sizes.
for (int[] groupIndices : groups) {
int groupMaxSize = 0;
// Compute the group's maximum size.
for (int groupIndice : groupIndices) {
int index = groupIndice - 1;
groupMaxSize = Math.max(groupMaxSize, sizes[index]);
}
// Set all sizes of this group to the group's maximum size.
for (int groupIndice : groupIndices) {
int index = groupIndice - 1;
sizes[index] = groupMaxSize;
}
}
return sizes;
}
/**
* Distributes free space over columns and rows and returns the sizes after this distribution
* process.
*
* @param formSpecs the column/row specifications to work with
* @param totalSize the total available size
* @param totalPrefSize the sum of all preferred sizes
* @param inputSizes the input sizes
* @return the distributed sizes
*/
private static int[] distributedSizes(List formSpecs,
int totalSize, int totalPrefSize,
int[] inputSizes) {
double totalFreeSpace = totalSize - totalPrefSize;
// Do nothing if there's no free space.
if (totalFreeSpace < 0) {
return inputSizes;
}
// Compute the total weight.
int count = formSpecs.size();
double totalWeight = 0.0;
for (int i = 0; i < count; i++) {
FormSpec formSpec = (FormSpec) formSpecs.get(i);
totalWeight += formSpec.getResizeWeight();
}
// Do nothing if there's no resizing column.
if (totalWeight == 0.0) {
return inputSizes;
}
int[] sizes = new int[count];
double restSpace = totalFreeSpace;
int roundedRestSpace = (int) totalFreeSpace;
for (int i = 0; i < count; i++) {
FormSpec formSpec = (FormSpec) formSpecs.get(i);
double weight = formSpec.getResizeWeight();
if (weight == FormSpec.NO_GROW) {
sizes[i] = inputSizes[i];
} else {
double roundingCorrection = restSpace - roundedRestSpace;
double extraSpace = totalFreeSpace * weight / totalWeight;
double correctedExtraSpace = extraSpace - roundingCorrection;
int roundedExtraSpace = (int) Math.round(correctedExtraSpace);
sizes[i] = inputSizes[i] + roundedExtraSpace;
restSpace -= extraSpace;
roundedRestSpace -= roundedExtraSpace;
}
}
return sizes;
}
/**
* Computes and returns a table that maps a column/row index to the maximum number of
* columns/rows that a component can span without spanning a growing column.
*
* Iterates over the specs from right to left/bottom to top, sets the table value to zero if a
* spec can grow, otherwise increases the span by one.
*
* Examples:
* "pref, 4dlu, pref, 2dlu, p:grow, 2dlu, pref" ->
* [4, 3, 2, 1, 0, MAX_VALUE, MAX_VALUE]
*
* "p:grow, 4dlu, p:grow, 9dlu, pref" ->
* [0, 1, 0, MAX_VALUE, MAX_VALUE]
*
* "p, 4dlu, p, 2dlu, 0:grow" ->
* [4, 3, 2, 1, 0]
*
*
* @param formSpecs the column specs or row specs
* @return a table that maps a spec index to the maximum span for fixed size specs
*/
private static int[] computeMaximumFixedSpanTable(List formSpecs) {
int size = formSpecs.size();
int[] table = new int[size];
int maximumFixedSpan = Integer.MAX_VALUE; // Could be 1
for (int i = size - 1; i >= 0; i--) {
FormSpec spec = (FormSpec) formSpecs.get(i); // ArrayList access
if (spec.canGrow()) {
maximumFixedSpan = 0;
}
table[i] = maximumFixedSpan;
if (maximumFixedSpan < Integer.MAX_VALUE) {
maximumFixedSpan++;
}
}
return table;
}
// Helper Code ************************************************************
/**
* Computes and returns the sum of integers in the given array of ints.
*
* @param sizes an array of ints to sum up
* @return the sum of ints in the array
*/
private static int sum(final int[] sizes) {
int sum = 0;
for (int i = sizes.length - 1; i >= 0; i--) {
sum += sizes[i];
}
return sum;
}
private static void invalidateAndRepaint(Container container) {
if (container == null) {
return;
}
if (container instanceof JComponent) {
((JComponent) container).revalidate();
} else {
container.invalidate();
}
container.repaint();
}
/**
* Checks and answers whether the given component with the specified CellConstraints shall be
* taken into account for the layout.
*
* @param component the component to test
* @param cc the component's associated CellConstraints
* @return {@code true} if a) {@code component} is visible, or b) {@code component} has no
* individual setting and the container-wide settings ignores the visibility, or c) {@code cc}
* indicates that this individual component ignores the visibility.
*/
private boolean takeIntoAccount(Component component, CellConstraints cc) {
return component.isVisible()
|| cc.honorsVisibility == null && !getHonorsVisibility()
|| Boolean.FALSE.equals(cc.honorsVisibility);
}
// Measuring Component Sizes ********************************************
/**
* An interface that describes how to measure a {@code Component}. Used to abstract from
* horizontal and vertical dimensions as well as minimum and preferred sizes.
*
* @since 1.1
*/
public static interface Measure {
/**
* Computes and returns the size of the given {@code Component}.
*
* @param component the component to measure
* @return the component's size
*/
int sizeOf(Component component);
}
/**
* An abstract implementation of the {@code Measure} interface that caches component sizes.
*/
private abstract static class CachingMeasure implements Measure, Serializable {
/**
* Holds previously requested component sizes. Used to minimize size requests to
* subcomponents.
*/
protected final ComponentSizeCache cache;
private CachingMeasure(ComponentSizeCache cache) {
this.cache = cache;
}
}
/**
* Measures a component by computing its minimum width.
*/
private static final class MinimumWidthMeasure extends CachingMeasure {
private MinimumWidthMeasure(ComponentSizeCache cache) {
super(cache);
}
@Override
public int sizeOf(Component c) {
return cache.getMinimumSize(c).width;
}
}
/**
* Measures a component by computing its minimum height.
*/
private static final class MinimumHeightMeasure extends CachingMeasure {
private MinimumHeightMeasure(ComponentSizeCache cache) {
super(cache);
}
@Override
public int sizeOf(Component c) {
return cache.getMinimumSize(c).height;
}
}
/**
* Measures a component by computing its preferred width.
*/
private static final class PreferredWidthMeasure extends CachingMeasure {
private PreferredWidthMeasure(ComponentSizeCache cache) {
super(cache);
}
@Override
public int sizeOf(Component c) {
return cache.getPreferredSize(c).width;
}
}
/**
* Measures a component by computing its preferred height.
*/
private static final class PreferredHeightMeasure extends CachingMeasure {
private PreferredHeightMeasure(ComponentSizeCache cache) {
super(cache);
}
@Override
public int sizeOf(Component c) {
return cache.getPreferredSize(c).height;
}
}
// Caching Component Sizes **********************************************
/**
* A cache for component minimum and preferred sizes. Used to reduce the requests to determine a
* component's size.
*/
private static final class ComponentSizeCache implements Serializable {
/**
* Maps components to their minimum sizes.
*/
private final Map minimumSizes;
/**
* Maps components to their preferred sizes.
*/
private final Map preferredSizes;
/**
* Constructs a {@code ComponentSizeCache}.
*
* @param initialCapacity the initial cache capacity
*/
private ComponentSizeCache(int initialCapacity) {
minimumSizes = new HashMap(initialCapacity);
preferredSizes = new HashMap(initialCapacity);
}
/**
* Invalidates the cache. Clears all stored size information.
*/
void invalidate() {
minimumSizes.clear();
preferredSizes.clear();
}
/**
* Returns the minimum size for the given component. Tries to look up the value from the
* cache; lazily creates the value if it has not been requested before.
*
* @param component the component to compute the minimum size
* @return the component's minimum size
*/
Dimension getMinimumSize(Component component) {
Dimension size = minimumSizes.get(component);
if (size == null) {
size = component.getMinimumSize();
minimumSizes.put(component, size);
}
return size;
}
/**
* Returns the preferred size for the given component. Tries to look up the value from the
* cache; lazily creates the value if it has not been requested before.
*
* @param component the component to compute the preferred size
* @return the component's preferred size
*/
Dimension getPreferredSize(Component component) {
Dimension size = preferredSizes.get(component);
if (size == null) {
size = component.getPreferredSize();
preferredSizes.put(component, size);
}
return size;
}
void removeEntry(Component component) {
minimumSizes.remove(component);
preferredSizes.remove(component);
}
}
// Exposing the Layout Information **************************************
/**
* Computes and returns the horizontal and vertical grid origins. Performs the same layout
* process as {@code #layoutContainer} but does not layout the components.
*
* This method has been added only to make it easier to debug the form layout. You must
* not call this method directly; It may be removed in a future release or the visibility may be
* reduced.
*
* @param parent the {@code Container} to inspect
* @return an object that comprises the grid x and y origins
*/
public LayoutInfo getLayoutInfo(Container parent) {
synchronized (parent.getTreeLock()) {
initializeColAndRowComponentLists();
Dimension size = parent.getSize();
Insets insets = parent.getInsets();
int totalWidth = size.width - insets.left - insets.right;
int totalHeight = size.height - insets.top - insets.bottom;
int[] x = computeGridOrigins(parent,
totalWidth, insets.left,
colSpecs,
colComponents,
colGroupIndices,
minimumWidthMeasure,
preferredWidthMeasure
);
int[] y = computeGridOrigins(parent,
totalHeight, insets.top,
rowSpecs,
rowComponents,
rowGroupIndices,
minimumHeightMeasure,
preferredHeightMeasure
);
return new LayoutInfo(x, y);
}
}
/**
* Stores column and row origins.
*/
public static final class LayoutInfo {
/**
* Holds the origins of the columns.
*/
public final int[] columnOrigins;
/**
* Holds the origins of the rows.
*/
public final int[] rowOrigins;
private LayoutInfo(int[] xOrigins, int[] yOrigins) {
this.columnOrigins = xOrigins;
this.rowOrigins = yOrigins;
}
/**
* Returns the layout's horizontal origin, the origin of the first column.
*
* @return the layout's horizontal origin, the origin of the first column.
*/
public int getX() {
return columnOrigins[0];
}
/**
* Returns the layout's vertical origin, the origin of the first row.
*
* @return the layout's vertical origin, the origin of the first row.
*/
public int getY() {
return rowOrigins[0];
}
/**
* Returns the layout's width, the size between the first and the last column origin.
*
* @return the layout's width.
*/
public int getWidth() {
return columnOrigins[columnOrigins.length - 1] - columnOrigins[0];
}
/**
* Returns the layout's height, the size between the first and last row.
*
* @return the layout's height.
*/
public int getHeight() {
return rowOrigins[rowOrigins.length - 1] - rowOrigins[0];
}
}
// Helper Code **********************************************************
/**
* Creates and returns a deep copy of the given array. Unlike {@code #clone} that performs a
* shallow copy, this method copies both array levels.
*
* @param array the array to clone
* @return a deep copy of the given array
*
* @see Object#clone()
*/
private static int[][] deepClone(int[][] array) {
int[][] result = new int[array.length][];
for (int i = 0; i < result.length; i++) {
result[i] = array[i].clone();
}
return result;
}
// Serialization ********************************************************
/**
* In addition to the default serialization mechanism this class invalidates the component size
* cache. The cache will be populated again after the deserialization. Also, the fields
* {@code colComponents} and {@code rowComponents} have been marked as transient to exclude them
* from the serialization.
*/
private void writeObject(ObjectOutputStream out) throws IOException {
invalidateCaches();
out.defaultWriteObject();
}
// Debug Helper Code ****************************************************
/*
// Prints the given column widths and row heights.
private void printSizes(String title, int[] colWidths, int[] rowHeights) {
System.out.println();
System.out.println(title);
int totalWidth = 0;
System.out.print("Column widths: ");
for (int i=0; i < getColumnCount(); i++) {
int width = colWidths[i];
totalWidth += width;
System.out.print(width + ", ");
}
System.out.println(" Total=" + totalWidth);
int totalHeight = 0;
System.out.print("Row heights: ");
for (int i=0; i < getRowCount(); i++) {
int height = rowHeights[i];
totalHeight += height;
System.out.print(height + ", ");
}
System.out.println(" Total=" + totalHeight);
System.out.println();
}
*/
}