// Table layout manager, with the flexibility of GridBagLayout but the ease
// of use of HTML table declarations.
// See http://www.parallax.co.uk/~rolf/download/table.html
// Copyright (C) Rolf Howarth 1997, 1998 (rolf@parallax.co.uk)
// Permission to freely use, modify and distribute this code is given,
// provided this notice remains attached. This code is provided for
// educational use only and no warranty as to its suitability for any
// other purpose is made.
// Modification history
// 0.1 01 Nov 96 First version
// 1.0 17 Jan 97 Minor bug fix; added column weighting.
// 1.1 08 Apr 98 Don't use methods deprecated in JDK1.1
// 1.2 16 Apr 98 Make own copy of Dimension objects as they're not immutable
import java.awt.*;
import java.util.*;
// Private class to parse and store the options for a single table entry
/**
* Table layout manager, with the flexibity of GridBagLayout but the ease of
* use of HTML table declarations.
*
* use like:
* new TableLayout(cols)
* add(comp, new TableOption(..))
* ..
*
*/
public class TableLayout implements LayoutManager,LayoutManager2 {
private Hashtable options = new Hashtable();
private TableOption defaultOption;
private int nrows=0, ncols=0;
private int ncomponents=0;
private Component[][] components=null;
private int MinWidth=0, MinHeight=0, PrefWidth=0, PrefHeight=0;
private int[] minWidth=null, minHeight=null, prefWidth=null, prefHeight=null;
private int[] weight=null, columnWidth=null;
private int hgap=0, vgap=0;
/**
* Construct a new table layout manager.
* @param cols Number of columns, used when adding components to tell when to go to the next row
* @param defaultAlignment Default defaultAlignment for cells if not specified at the time of adding the component
* @param hgap Horizontal gap between cells and at edge (in pixels)
* @param vgap Vertical gap between cells and at edge (in pixels)
**/
public TableLayout(int cols, String defaultAlignment, int hgap, int vgap) {
this(cols, new TableOption(defaultAlignment),hgap,vgap);
}
public TableLayout(int cols, TableOption defaultAlignment, int hgap, int vgap) {
this.ncols = cols; // the number of columns is specified
this.nrows = 0; // the number of rows is calculated
this.components = new Component[cols][];
this.defaultOption=defaultAlignment;
this.hgap = hgap;
this.vgap = vgap;
}
public TableLayout(int cols, String alignment)
{
this(cols, alignment, 0, 0);
}
public TableLayout(int cols)
{
this(cols, "", 0, 0);
}
public void addLayoutComponent(String alignment, Component comp)
{
options.put(comp, new TableOption(alignment));
}
public void removeLayoutComponent(Component comp)
{
options.remove(comp);
}
// Iterate through the components, counting the number of rows taking into account
// row and column spanning, then initialise the components[c][r] matrix so that
// we can retrieve the component at a particular row,column position.
private void loadComponents(Container parent)
{
ncomponents = parent.getComponentCount();
// If we haven't allocated the right sized array for each column yet, do so now.
// Note that the number of columns is fixed, but the number of rows is not know
// and could in the worst case be up the number of components. Unfortunately this
// means we need to allocate quite big arrays, but the alternative would require
// complex multiple passes as we try to work out the effect of row spanning.
if (components[0] == null || components[0].length < ncomponents)
{
for (int i=0; i components[i] = new Component[ncomponents];
}
// Nullify the array
for (int i=0; i {
for (int j=0; j components[i][j] = null;
}
// fill the matrix with components, taking row/column spanning into account
int row=0, col=0;
for (int i=0; i {
// get the next component and its options
Component comp = parent.getComponent(i);
TableOption option = (TableOption) options.get(comp);
if (option==null) option = defaultOption;
// handle options to force us to column 0 or to skip columns
if (option.forceColumn >= 0)
{
if (col > option.forceColumn)
++row;
col = option.forceColumn;
}
col += option.skipColumns;
if (col>=ncols) { ++row; col=0; }
// skip over any cells that are already occupied
while (components[col][row] != null)
{
++col;
if (col>=ncols) { ++row; col=0; }
}
// if using colspan, will we fit on this row?
if (col+option.colSpan > ncols)
{
++row;
col = 0;
}
// for now, fill all the cells that are occupied by this component
for (int c=0; c for (int r=0; r components[col+c][row+r] = comp;
// advance to the next cell, ready for the next component
col += option.colSpan;
if (col>=ncols) { ++row; col=0; }
}
// now we know how many rows there are
if (col == 0)
nrows = row;
else
nrows = row+1;
// now we've positioned our components we can thin out the cells so
// we only remember the top left corner of each component
for (row=0; row {
for (col=0; col {
Component comp = components[col][row];
for (int r=row; r {
for (int c=col; c {
if (r>row || c>col)
components[c][r] = null;
}
}
}
}
}
private void measureComponents(Container parent)
{
// set basic metrics such as ncomponents & nrows, and load the components
// into the components[][] array.
loadComponents(parent);
// allocate new arrays to store row and column preferred and min sizes, but
// only if the old arrays aren't big enough
if (minWidth==null || minWidth.length {
minWidth = new int[ncols];
prefWidth = new int[ncols];
columnWidth = new int[ncols];
weight = new int[ncols];
}
if (minHeight==null || minHeight.length {
minHeight = new int[nrows];
prefHeight = new int[nrows];
}
int i;
for (i=0; i {
minWidth[i] = 0;
prefWidth[i] = 0;
}
for (i=0; i {
minHeight[i] = 0;
prefHeight[i] = 0;
}
// measure the minimum and preferred size of each row and column
for (int row=0; row {
for (int col=0; col {
Component comp = components[col][row];
if (comp != null)
{
TableOption option = (TableOption) options.get(comp);
if (option==null) option = defaultOption;
Dimension minSize = new Dimension(comp.getMinimumSize());
Dimension prefSize = new Dimension(comp.getPreferredSize());
// enforce prefSize>=minSize
if (prefSize.width < minSize.width)
prefSize.width = minSize.width;
if (prefSize.height < minSize.height)
prefSize.height = minSize.height;
// divide size across all the rows or columns being spanned
minSize.width /= option.colSpan;
minSize.height /= option.rowSpan;
prefSize.width = (prefSize.width - hgap*(option.colSpan-1)) / option.colSpan;
prefSize.height = (prefSize.height - vgap*(option.rowSpan-1)) / option.rowSpan;
for (int c=0; c {
if (minSize.width > minWidth[col+c])
minWidth[col+c] = minSize.width;
if (prefSize.width > prefWidth[col+c])
prefWidth[col+c] = prefSize.width;
}
for (int r=0; r {
if (minSize.height > minHeight[row+r])
minHeight[row+r] = minSize.height;
if (prefSize.height > prefHeight[row+r])
prefHeight[row+r] = prefSize.height;
}
}
}
}
// add rows and columns to give total min and preferred size of whole grid
MinWidth = 0;
MinHeight = 0;
PrefWidth = hgap;
PrefHeight = vgap;
for (i=0; i {
MinWidth += minWidth[i];
PrefWidth += prefWidth[i] + hgap;
}
for (i=0; i {
MinHeight += minHeight[i];
PrefHeight += prefHeight[i] + vgap;
}
}
public Dimension minimumLayoutSize(Container parent)
{
Insets insets = parent.getInsets();
measureComponents(parent);
// System.out.println("Min Size: "+MinWidth+","+MinHeight);
return new Dimension(insets.left + insets.right + MinWidth,
insets.top + insets.bottom + MinHeight);
}
public Dimension preferredLayoutSize(Container parent)
{
Insets insets = parent.getInsets();
measureComponents(parent);
// System.out.println("Pref Size: "+PrefWidth+","+PrefHeight);
// System.out.println("+ insets LR "+insets.left+"+"+insets.right+", TB "+insets.top+"+"+insets.bottom);
return new Dimension(insets.left + insets.right + PrefWidth,
insets.top + insets.bottom + PrefHeight);
}
public void layoutContainer(Container parent)
{
Insets insets = parent.getInsets();
measureComponents(parent);
int width = parent.getSize().width - (insets.left + insets.right);
int height = parent.getSize().height - (insets.top + insets.bottom);
// System.out.println("Resize "+width+","+height);
// Decide whether to base our scaling on minimum or preferred sizes, or
// a mixture of both, separately for width and height scaling.
// This weighting also tells us how much of the hgap/vgap to use.
double widthWeighting = 0.0;
if (width >= PrefWidth || PrefWidth==MinWidth)
widthWeighting = 1.0;
else if (width <= MinWidth)
{
widthWeighting = 0.0;
width = MinWidth;
}
else
widthWeighting = (double)(width-MinWidth)/(double)(PrefWidth-MinWidth);
double heightWeighting = 0.0;
if (height >= PrefHeight || PrefHeight==MinHeight)
heightWeighting = 1.0;
else if (height <= MinHeight)
{
heightWeighting = 0.0;
height = MinHeight;
}
else
heightWeighting = (double)(height-MinHeight)/(double)(PrefHeight-MinHeight);
// calculate scale factors to scale components to size of container, based
// on weighted combination of minimum and preferred sizes
double minWidthScale = (1.0 - widthWeighting) * width/MinWidth;
//double prefWidthScale = widthWeighting * (width-hgap*(ncols+1))/(PrefWidth-hgap*(ncols+1));
double minHeightScale = (1.0 - heightWeighting) * height/MinHeight;
double prefHeightScale = heightWeighting * (height-vgap*(nrows+1))/(PrefHeight-vgap*(nrows+1));
// only get the full amount of gap if we're working to preferred size
int vGap = (int) (vgap * heightWeighting);
int hGap = (int) (hgap * widthWeighting);
int y = insets.top + vGap;
for (int c=0; c weight[c] = prefWidth[c];
for (int r=0; r {
int x = insets.left + hGap;
int rowHeight = (int)(minHeight[r]*minHeightScale + prefHeight[r]*prefHeightScale);
// Column padding can vary from row to row, so we need several
// passes through the columns for each row:
// First, work out the weighting that deterimines how we distribute column padding
for (int c=0; c {
Component comp = components[c][r];
if (comp != null)
{
TableOption option = (TableOption) options.get(comp);
if (option==null) option = defaultOption;
if (option.weight >= 0)
weight[c] = option.weight;
else if (option.weight == -1)
weight[c] = prefWidth[c];
}
}
int totalWeight = 0;
for (int c=0; c totalWeight += weight[c];
int horizSurplus = width - hgap*(ncols+1) - PrefWidth;
// Then work out column sizes, essentially preferred size + share of padding
for (int c=0; c {
columnWidth[c] = (int) (minWidthScale * minWidth[c] + widthWeighting * prefWidth[c]);
if (horizSurplus > 0 && totalWeight > 0)
columnWidth[c] += (int) (widthWeighting * horizSurplus * weight[c] / totalWeight);
}
// Only now do we know enough to position all the columns within this row...
for (int c=0; c {
Component comp = components[c][r];
if (comp != null)
{
TableOption option = (TableOption) options.get(comp);
if (option==null) option = defaultOption;
// cell size may be bigger than row/column size due to spanning
int cellHeight = rowHeight;
int cellWidth = columnWidth[c];
for (int i=1; i cellWidth += columnWidth[c+i];
for (int i=1; i cellHeight += (int)(minHeight[r+i]*minHeightScale + prefHeight[r+i]*prefHeightScale + vGap);
Dimension d = new Dimension(comp.getPreferredSize());
if (d.width > cellWidth || option.horizontal==TableOption.FILL)
d.width = cellWidth;
if (d.height > cellHeight || option.vertical==TableOption.FILL)
d.height = cellHeight;
int yoff = 0;
if (option.vertical == TableOption.BOTTOM)
yoff = cellHeight - d.height;
else if (option.vertical == TableOption.CENTRE)
yoff = (cellHeight - d.height) / 2;
int xoff = 0;
if (option.horizontal == TableOption.RIGHT)
xoff = cellWidth - d.width;
else if (option.horizontal == TableOption.CENTRE)
xoff = (cellWidth - d.width) / 2;
// System.out.println(" "+comp.getClass().getName()+" at ("+x+"+"+xoff+","+y+"+"+yoff+"), size "+d.width+","+d.height);
comp.setBounds(x+xoff,y+yoff,d.width,d.height);
}
x += columnWidth[c] + hGap;
}
y += rowHeight + vGap;
}
}
public void addLayoutComponent(Component comp, Object constraints) {
if(constraints instanceof TableOption){
options.put(comp, constraints);
}
else if(constraints==null){
options.put(comp,defaultOption);
}
else throw new IllegalArgumentException("not a valid constraints object="+constraints);
}
/**
* 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 furthest away from the origin, 0.5 is centered, etc.
*
* @return the value 0.5f
to indicate centered
*/
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 furthest away from the origin, 0.5 is centered, etc.
*
* @return the value 0.5f
to indicate centered
*/
public float getLayoutAlignmentY(Container parent) {
return 0.5f;
}
/**
* Invalidates the layout, indicating that if the layout manager
* has cached information it should be discarded.
*/
public void invalidateLayout(Container target) {
}
/**
* 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
*/
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
}
class TableOption{
public static final int CENTRE=1, FILL=2, LEFT=3, RIGHT=4, TOP=5, BOTTOM=6;
int horizontal = CENTRE;
int vertical = CENTRE;
int rowSpan=1, colSpan=1, skipColumns=0, forceColumn=-1, weight=-2;
/**
*
* @param horizontal one of CENTRE,FILL,LEFT,RIGHT,TOP,BOTTOM
* @param vertical
*/
public TableOption(int horizontal, int vertical) {
this.horizontal = horizontal;
this.vertical = vertical;
}
public TableOption(int horizontal, int vertical, int rowSpan, int colSpan) {
this.horizontal = horizontal;
this.vertical = vertical;
this.rowSpan = rowSpan;
this.colSpan = colSpan;
}
public TableOption(int horizontal, int vertical, int rowSpan, int colSpan, int skipColumns, int forceColumn, int weight) {
this.horizontal = horizontal;
this.vertical = vertical;
this.rowSpan = rowSpan;
this.colSpan = colSpan;
this.skipColumns = skipColumns;
this.forceColumn = forceColumn;
this.weight = weight;
}
TableOption(String alignment) {
StringTokenizer tk = new StringTokenizer(alignment, ",");
while (tk.hasMoreTokens())
{
String token = tk.nextToken();
boolean ok = false;
int delim = token.indexOf("=");
if (token.equals("NW") || token.equals("W") || token.equals("SW"))
{ horizontal = LEFT; ok=true; }
if (token.equals("NE") || token.equals("E") || token.equals("SE"))
{ horizontal = RIGHT; ok=true; }
if (token.equals("N") || token.equals("C") || token.equals("F"))
{ horizontal = CENTRE; ok=true; }
if (token.equals("F") || token.equals("FH"))
{ horizontal = FILL; ok=true; }
if (token.equals("N") || token.equals("NW") || token.equals("NE"))
{ vertical = TOP; ok=true; }
if (token.equals("S") || token.equals("SW") || token.equals("SE"))
{ vertical = BOTTOM; ok=true; }
if (token.equals("W") || token.equals("C") || token.equals("E"))
{ vertical = CENTRE; ok=true; }
if (token.equals("F") || token.equals("FV"))
{ vertical = FILL; ok=true; }
if (delim>0)
{
int val = Integer.parseInt(token.substring(delim+1));
token = token.substring(0,delim);
if (token.equals("CS") && val>0)
{ colSpan = val; ok=true; }
else if (token.equals("RS") && val>0)
{ rowSpan = val; ok=true; }
else if (token.equals("SKIP") && val>0)
{ skipColumns = val; ok=true; }
else if (token.equals("COL"))
{ forceColumn = val; ok=true; }
else if (token.equals("WT"))
{ weight = val; ok=true; }
}
if (!ok) throw new IllegalArgumentException("TableOption "+token);
}
}
}