package org.openslx.dozmod.gui.helper;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import javax.swing.Box;
import javax.swing.JPanel;
/**
* Helper class for using the GridBagLayout.
*/
public class GridManager {
/**
* Setting this to true will insert green panels in cells where nothing was
* added
*/
public static boolean debugEmptyCells = true;
private final Container container;
private final Insets defaultInsets;
private final int columnCount;
private final boolean strict;
private int nextColumn = 0;
private int currentRow = 0;
private boolean valid = true;
private final ArrayList<Component[]> currentRows = new ArrayList<>();
private GBC currentGbc = null;
// Static general constraints
private static final GridBagConstraints emptyFiller = new GridBagConstraints(0, 0, 1, 1, 0, 0,
GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0);
/**
* Create a new GridManager for the given component. The manager will use
* strict mode and use an inset of 1px for every side of every cell.
*
* @param container The component to apply the layout to
* @param columnCount The number of columns per row
*/
public GridManager(Container container, int columnCount) {
this(container, columnCount, true);
}
/**
* Create a new GridManager for the given component. The manager will use an
* inset of 1px for every side of every cell.
*
* @param container The component to apply the layout to
* @param columnCount The number of columns per row
* @param strict If true, the manager will ensure you call
* {@link #nextRow()} after finishing each row, and that you
* don't call it if the current row is empty. Otherwise, the
* manager will silently advance to the next row if the current
* row is full, and will ignore calls to {@link #nextRow()} if
* the current row is empty
*/
public GridManager(Container container, int columnCount, boolean strict) {
this(container, columnCount, strict, new Insets(1, 1, 1, 1));
}
/**
* Create a new GridManager for the given component. The manager will use an
* inset of 1px for every side of every cell.
*
* @param container The component to apply the layout to
* @param columnCount The number of columns per row
* @param strict If true, the manager will ensure you call
* {@link #nextRow()} after finishing each row, and that you
* don't call it if the current row is empty. Otherwise, the
* manager will silently advance to the next row if the current
* row is full, and will ignore calls to {@link #nextRow()} if
* the current row is empty
* @param defaultInsets an {@link Insets} instance to use for every cell by
* default
*/
public GridManager(Container container, int columnCount, boolean strict, Insets defaultInsets) {
this.defaultInsets = defaultInsets;
this.container = container;
this.columnCount = columnCount;
this.strict = strict;
this.container.setLayout(new GridBagLayout());
this.currentRows.add(new Component[columnCount]);
}
/**
* Add the given component to the next free grid cell.
*
* @param component Component to add
* @return A {@link GBC} instance that can be used to further influence the
* behavior of this component
* @throws IllegalArgumentException If there aren't enough columns left in
* the current row to add the given component with the desired
* horizontal span, or if one of the span parameters is
* <code>< 1</code>
*/
public GBC add(Component component) {
return add(component, 1, 1);
}
/**
* Add the given component to the next free grid cell, applying the given
* horizontal cell-span.
*
* @param component Component to add
* @param spanX horizontal span
* @return A {@link GBC} instance that can be used to further influence the
* behavior of this component
* @throws IllegalArgumentException If there aren't enough columns left in
* the current row to add the given component with the desired
* horizontal span, or if one of the span parameters is
* <code>< 1</code>
*/
public GBC add(Component component, int spanX) {
return add(component, spanX, 1);
}
/**
* Add the given component to the next free grid cell, applying the given
* horizontal and vertical cell-span.
*
* @param component Component to add
* @param spanX horizontal span
* @param spanY vertical span
* @return A {@link GBC} instance that can be used to further influence the
* behavior of this component
* @throws IllegalArgumentException If there aren't enough columns left in
* the current row to add the given component with the desired
* horizontal span, or if one of the span parameters is
* <code>< 1</code>
*/
public GBC add(Component component, int spanX, int spanY) {
checkValid();
if (spanX < 1 || spanY < 1)
throw new IllegalArgumentException("Span must be >= 1");
// Automatically advance to next row if strict mode is not enabled and we're at the end of the current one
if (!strict && nextColumn == columnCount) {
nextRow();
}
if (!hasFreeColumns(spanX))
throw new IllegalArgumentException("Cannot add component: Not enough columns left in row");
addCurrentControl();
currentGbc = new GBC(component, spanX, spanY);
nextColumn += spanX;
skipToFreeColumn();
return currentGbc;
}
/**
* Convenience method for adding an empty placeholder in the current cell.
*/
public GBC skip() {
return add(Box.createGlue());
}
/**
* Convenience method for adding an empty placeholder in the current cell.
*
* @param spanX horizontal span
*/
public GBC skip(int spanX) {
return add(Box.createGlue(), spanX);
}
/**
* Convenience method for adding an empty placeholder in the current cell.
*
* @param spanX horizontal span
* @param spanY vertical span
*/
public GBC skip(int spanX, int spanY) {
return add(Box.createGlue(), spanX, spanY);
}
/**
* Advance to next row.
*
* @throws IllegalStateException if strict mode is enabled and the current
* row is empty
*/
public void nextRow() {
checkValid();
if (nextColumn == 0 && currentRows.size() == 1 && allCellsEmpty()) {
if (strict)
throw new IllegalStateException("Cannot call nextRow when current row is empty");
return;
}
addCurrentControl();
if (nextColumn < columnCount && debugEmptyCells) {
emptyFiller.gridy = currentRow;
Component[] row = currentRows.get(0);
for (int i = nextColumn; i < columnCount; ++i) {
if (row[i] != null)
continue;
JPanel p = new JPanel();
p.setBackground(Color.GREEN);
emptyFiller.gridx = i;
container.add(p, emptyFiller);
}
}
currentRow++;
if (currentRows.size() == 1) {
Component[] row = currentRows.get(0);
for (int i = 0; i < columnCount; ++i) {
row[i] = null;
}
} else {
currentRows.remove(0);
}
nextColumn = 0;
skipToFreeColumn();
}
/**
* Finish the layout. Further calls to the <code>add</code>-methods will
* result in an exception being thrown
*
* @param addVerticalGlue Whether to add expanding vertical glue to the
* layout, so all components will be pushed to the top of the
* container
*/
public void finish(boolean addVerticalGlue) {
checkValid();
if (nextColumn != 0) {
nextRow();
}
if (addVerticalGlue) {
while (currentRows.size() > 1 || !allCellsEmpty()) {
nextRow();
}
add(Box.createGlue(), columnCount, 1).expand(true, true).fill(true, true);
nextRow();
}
valid = false;
}
// Private helpers
private boolean hasFreeColumns(int num) {
num--;
Component[] row = currentRows.get(0);
for (int i = nextColumn; i < columnCount; ++i) {
if (row[i] != null)
return false;
if (i - nextColumn >= num)
return true;
}
return false;
}
private void addCurrentControl() {
if (currentGbc == null)
return;
container.add(currentGbc.component, currentGbc);
// Remember placement for future sanity checks
for (int relrow = 0; relrow < currentGbc.gridheight; ++relrow) {
if (currentRows.size() <= relrow) {
currentRows.add(new Component[columnCount]);
}
Component[] row = currentRows.get(relrow);
for (int relcol = 0; relcol < currentGbc.gridwidth; ++relcol) {
if (row[relcol + currentGbc.gridx] != null)
throw new IllegalStateException("Collision detected in cell ("
+ (relcol + currentGbc.gridx) + "|" + (relrow + currentGbc.gridy) + "): Have "
+ row[relcol + currentGbc.gridx].getClass().getSimpleName() + ", trying to add "
+ currentGbc.component.getClass().getSimpleName());
row[relcol + currentGbc.gridx] = currentGbc.component;
}
}
currentGbc.valid = false;
currentGbc = null;
}
private boolean allCellsEmpty() {
Component[] row = currentRows.get(0);
for (int i = 0; i < columnCount; ++i) {
if (row[i] != null)
return false;
}
return true;
}
private void checkValid() {
if (!valid)
throw new IllegalStateException("Layout is already finalized!");
}
private void skipToFreeColumn() {
Component[] row = currentRows.get(0);
while (nextColumn < columnCount && row[nextColumn] != null) {
nextColumn++;
}
}
//
@SuppressWarnings("serial")
public class GBC extends GridBagConstraints {
private boolean valid = true;
private final Component component;
/**
* Set the fill properties of the element being added.
*
* @param fillX Fill the cell horizontally, enlarging the control
* @param fillY Fill the cell vertically, enlarging the control
* @return This instance, so calls can be chained
*/
public GBC fill(boolean fillX, boolean fillY) {
checkValid();
if (fillX && fillY) {
this.fill = GridBagConstraints.BOTH;
} else if (fillX) {
this.fill = GridBagConstraints.HORIZONTAL;
} else if (fillY) {
this.fill = GridBagConstraints.VERTICAL;
} else {
this.fill = GridBagConstraints.NONE;
}
return this;
}
/**
* Set the expand properties of the element being added.
* If multiple components are set to expand, they'll distribute the
* excess space among them. This call is equal to {@link #expand(1, 1)}
*
* @param expandX Assign any remaining horizontal space to this cell
* @param expandY Assign any remaining vertical space to this cell
* @return This instance, so calls can be chained
*/
public GBC expand(boolean expandX, boolean expandY) {
return expand(expandX ? 1 : 0, expandY ? 1 : 0);
}
/**
* Set the expand properties of the element being added.
* If multiple components are set to expand, they'll distribute the
* excess space among them, using the given weights. Setting one of the
* values to 0 will disable expanding for that axis.
*
* @param expandX Weight for distribution of remaining horizontal space
* @param expandY Weight for distribution of remaining vertical space
* @return This instance, so calls can be chained
*/
public GBC expand(double expandX, double expandY) {
checkValid();
this.weightx = expandX;
this.weighty = expandY;
return this;
}
/**
* Set the anchor field of this {@link GridBagConstraints} instance.
*
* @param value
* @return This instance, so calls can be chained
*/
public GBC anchor(int value) {
checkValid();
this.anchor = value;
return this;
}
/**
* Set the insets field of this {@link GridBagConstraints} instance.
*
* @param insets
* @return This instance, so calls can be chained
*/
public GBC insets(Insets insets) {
checkValid();
this.insets = insets;
return this;
}
// Extend with more helpers as needed
private GBC(Component component, int spanX, int spanY) {
this.gridx = nextColumn;
this.gridy = currentRow;
this.gridwidth = spanX;
this.gridheight = spanY;
this.weightx = 0;
this.weighty = 0;
this.anchor = GridBagConstraints.LINE_START;
this.fill = GridBagConstraints.NONE;
this.insets = defaultInsets;
this.ipadx = 0;
this.ipady = 0;
this.component = component;
}
private void checkValid() {
if (!valid)
throw new IllegalAccessError("Cannot modify constraints after adding next control");
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{ ");
for (Field f : getClass().getFields()) {
if (Modifier.isFinal(f.getModifiers()) || Modifier.isStatic(f.getModifiers()))
continue;
String val;
try {
val = f.get(this).toString();
} catch (IllegalArgumentException | IllegalAccessException e) {
val = "???";
}
sb.append(f.getName());
sb.append('=');
sb.append(val);
sb.append(' ');
}
sb.append('}');
return sb.toString();
}
}
}