summaryrefslogblamecommitdiffstats
path: root/dozentenmodul/src/main/java/org/openslx/dozmod/gui/helper/GridManager.java
blob: b3b5f70e163c6697a61d715321964ff051679590 (plain) (tree)
1
2
3
4
5
6
7
8
9
10



                                      
                          


                                   

                                  


                           










                                                                                   
                                                     
 
                                          








                                                                             













                                                                                                              
                                                                  















                                                                                    
                                                                                  

















                                                                                    
                                                                                                        
























                                                                                   
















                                                                                   





























                                                                                                                          

























                                                                                  













































































                                                                                                                 
                                                                
                                                              
                                                                                



                                                                            

                                                                                       
                                                                                                      



                                                                                                                                        




























                                                                                        

                                                     












                                                                                 
                                                                    
                                           
                                                                          
                                           


                                                                        






                                                                                   
                                                                                       





                                                                                    
                                                                        












                                                                                       
                                     

                                               


                                    



                                                                                    
                                                                 






                                              











                                                                                    


                                                                        










                                                                    






                                                                                                                    





















                                                                                                              


         
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>&lt; 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>&lt; 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>&lt; 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();
		}
	}

}