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 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 * < 1 */ 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 * < 1 */ 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 * < 1 */ 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 add-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(); } } }