blob: c5bbe4b597bf37a640574fa2d67144ad2287a058 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2005 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.layout;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Layout;
/**
* <p>Instance of this class lay out the control children of a <code>Composite</code>
* in a grid, using a simple set of rules and a simple API. This class is
* intended to be more predictable than <code>GridLayout</code> and easier to use than
* <code>FormLayout</code>, while retaining most of the power of both.</p>
*
* <p>The power of a <code>CellLayout</code> lies in the ability to control
* the size and resizing properties of each row and column. Unlike other layout
* classes, complex layouts can be created without attaching any layout data to
* individual controls in the layout. </p>
*
* <p>The various subclasses of <code>IColumnInfo</code>
* can be used to create columns with fixed width, columns whose width is computed
* from child controls, or width that grows in proportion to the size of other
* columns. Layouts can be given a default <code>IColumnInfo</code> that will
* be used to set the size of any column whose properties have not been explicitly
* set. This is useful for creating layouts where most or all columns have the
* same properties. Similarly, the subclasses of <code>IRowInfo</code> can be used to
* control the height of individual rows.</p>
*
* <p>For a finer grain of control, <code>CellData</code> objects can be attached
* to individual controls in the layout. These objects serve a similar function as
* <code>GridData</code> objects serve for <code>GridLayout</code>. They allow
* controls to span multiple rows or columns, set the justification of the control
* within its cell, and allow the user to override the preferred size of the control.
* </p>
*
* <p>In many cases, it is not necessary to attach any layout data to controls in
* the layout, since the controls can be arranged based on the properties of rows
* and columns. However, layout data may be attached to individual controls to
* allow them to span multiple columns or to control their justification within
* their cell.
* </p>
*
* <p>All the <code>set</code> methods in this class return <code>this</code>, allowing
* a layout to be created and initialized in a single line of code. For example: </p>
*
* <code>
* Composite myControl = new Composite(parent, SWT.NONE);
* myControl.setLayout(new CellLayout(2).setMargins(10,10).setSpacing(5,5));
* </code>
*
* @since 3.0
*/
public class CellLayout extends Layout {
/**
* Object used to compute the height of rows whose properties have not been
* explicitly set.
*/
private Row defaultRowSettings = new Row(false);
/**
* Object used to compute the width of columns whose properties have not been
* explicitly set.
*/
private Row defaultColSettings = new Row(true);
/**
* horizontalSpacing specifies the number of pixels between the right
* edge of one cell and the left edge of its neighbouring cell to
* the right.
*
* The default value is 5.
*/
int horizontalSpacing = 5;
/**
* verticalSpacing specifies the number of pixels between the bottom
* edge of one cell and the top edge of its neighbouring cell underneath.
*
* The default value is 5.
*/
int verticalSpacing = 5;
/**
* marginWidth specifies the number of pixels of horizontal margin
* that will be placed along the left and right edges of the layout.
*
* The default value is 0.
*/
public int marginWidth = 5;
/**
* marginHeight specifies the number of pixels of vertical margin
* that will be placed along the top and bottom edges of the layout.
*
* The default value is 0.
*/
public int marginHeight = 5;
/**
* Number of columns in this layout, or 0 indicating that the whole layout
* should be on a single row.
*/
private int numCols;
/**
* List of IColumnInfo. The nth object is used to compute the width of the
* nth column, or null indicating that the default column should be used.
*/
private List cols;
/**
* List of RowInfo. The nth object is used to compute the height of the
* nth row, or null indicating that the default row should be used.
*/
private List rows = new ArrayList(16);
// Cached information
private GridInfo gridInfo = new GridInfo();
private int[] cachedRowMin = null;
private int[] cachedColMin = null;
public static int cacheMisses;
public static int cacheHits;
private LayoutCache cache = new LayoutCache();
// End of cached control sizes
/**
* Creates the layout
*
* @param numCols the number of columns in this layout,
* or 0 indicating that the whole layout should be on one row.
*/
public CellLayout(int numCols) {
super();
this.numCols = numCols;
cols = new ArrayList(numCols == 0 ? 3 : numCols);
}
/**
* Sets the amount empty space between cells
*
* @param newSpacing a point (x,y) corresponding to the number of pixels of
* empty space between adjacent columns and rows respectively
*/
public CellLayout setSpacing(int horizontalSpacing, int verticalSpacing) {
this.horizontalSpacing = horizontalSpacing;
this.verticalSpacing = verticalSpacing;
return this;
}
/**
* Sets the amount empty space between cells
*
* @param newSpacing a point (x,y) corresponding to the number of pixels of
* empty space between adjacent columns and rows respectively
*/
public CellLayout setSpacing(Point newSpacing) {
horizontalSpacing = newSpacing.x;
verticalSpacing = newSpacing.y;
return this;
}
/**
* Returns the amount of empty space between adjacent cells
*
* @return a point (x,y) corresponding to the number of pixels of empty
* space between adjacent columns and rows respectively
*/
public Point getSpacing() {
return new Point(horizontalSpacing, verticalSpacing);
}
/**
* Sets the size of the margin around the outside of the layout.
*
* @param marginWidth the size of the margin around the top and
* bottom of the layout
* @param marginHeight the size of the margin on the left and right
* of the layout.
*/
public CellLayout setMargins(int marginWidth, int marginHeight) {
this.marginWidth = marginWidth;
this.marginHeight = marginHeight;
return this;
}
/**
* Sets the size of the margin around the outside of the layout.
*
* @param newMargins point indicating the size of the horizontal and vertical
* margins, in pixels.
*/
public CellLayout setMargins(Point newMargins) {
marginWidth = newMargins.x;
marginHeight = newMargins.y;
return this;
}
/**
* Returns the size of the margins around the outside of the layout.
*
* @return the size of the outer margins, in pixels.
*/
public Point getMargins() {
return new Point(marginWidth, marginHeight);
}
/**
* Sets the default column settings. All columns will use these settings unless
* they have been explicitly assigned custom settings by setColumn.
*
* @param info the properties of all default columns
* @see setColumn
*/
public CellLayout setDefaultColumn(Row info) {
defaultColSettings = info;
return this;
}
/**
* Sets the column info for the given column number (the leftmost column is column 0).
* This replaces any existing info for the column. Note that more than one column
* are allowed to share the same IColumnInfo instance if they have identical properties.
*
* @param colNum the column number to modify
* @param info the properties of the column, or null if this column should use the
* default properties
*/
public CellLayout setColumn(int colNum, Row info) {
while (cols.size() <= colNum) {
cols.add(null);
}
cols.set(colNum, info);
return this;
}
/**
* Sets the default row settings for this layout. Unless this is overridden
* for an individual row, all rows will use the default settings.
*
* @param info the row info object that should be used to set the size
* of rows, by default.
*/
public CellLayout setDefaultRow(Row info) {
defaultRowSettings = info;
return this;
}
/**
* Sets the row info for the given rows. The topmost row is row 0. Multiple
* rows are allowed to share the same RowInfo instance.
*
* @param rowNum the row number to set
* @param info the row info that will control the sizing of the given row,
* or null if the row should use the default settings for this layout.
*/
public CellLayout setRow(int rowNum, Row info) {
while (rows.size() <= rowNum) {
rows.add(null);
}
rows.set(rowNum, info);
return this;
}
/**
* Returns the row info that controls the size of the given row. Will return
* the default row settings for this layout if no custom row info has been
* assigned to the row.
*
* @param rowNum
* @return
*/
private Row getRow(int rowNum, boolean isHorizontal) {
if (isHorizontal) {
if (rowNum >= rows.size()) {
return defaultRowSettings;
}
Row result = (Row) rows.get(rowNum);
if (result == null) {
result = defaultRowSettings;
}
return result;
} else {
if (rowNum >= cols.size()) {
return defaultColSettings;
}
Row result = (Row) cols.get(rowNum);
if (result == null) {
result = defaultColSettings;
}
return result;
}
}
/**
* Initializes the gridInfo object.
*
* @param children controls that are being layed out
*/
private void initGrid(Control[] children) {
cache.setControls(children);
gridInfo.initGrid(children, this);
cachedRowMin = null;
cachedColMin = null;
}
/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Layout#computeSize(org.eclipse.swt.widgets.Composite, int, int, boolean)
*/
protected Point computeSize(Composite composite, int wHint, int hHint,
boolean flushCache) {
Control[] children = composite.getChildren();
initGrid(children);
if (flushCache) {
cache.flush();
}
// Determine the amount of whitespace (area that cannot be used by controls)
Point emptySpace = totalEmptySpace();
int[] heightConstraints = computeConstraints(true);
int width;
if (wHint == SWT.DEFAULT) {
width = preferredSize(heightConstraints, false);
} else {
width = wHint - emptySpace.x;
}
int height = hHint;
if (hHint == SWT.DEFAULT) {
height = preferredSize(
computeSizes(heightConstraints, width, false), true);
} else {
height = hHint - emptySpace.y;
}
Point preferredSize = new Point(width + emptySpace.x, height
+ emptySpace.y);
// At this point we know the layout's preferred size. Now adjust it
// if we're smaller than the minimum possible size for the composite.
// If exactly one dimension of our preferred size is smaller than
// the minimum size of our composite, then set that dimension to
// the minimum size and recompute the other dimension (for example,
// increasing the width to match a shell's minimum width may reduce
// the height allocated for a wrapping text widget). There is no
// point in doing this if both dimensions are smaller than the
// composite's minimum size, since we're already smaller than
// we need to be.
Point minimumSize = CellLayoutUtil.computeMinimumSize(composite);
boolean wider = (preferredSize.x >= minimumSize.x);
boolean taller = (preferredSize.y >= minimumSize.y);
if (wider) {
if (taller) {
// If we're larger in both dimensions, don't adjust the minimum
// size.
return preferredSize;
} else {
// If our preferred height is smaller than the minimum height,
// recompute the preferred width using the minimum height
return computeSize(composite, wHint, minimumSize.y, false);
}
} else {
if (taller) {
// If our preferred width is smaller than the minimum width,
// recompute the preferred height using the minimum width
return computeSize(composite, minimumSize.x, hHint, false);
} else {
// If both dimensions are smaller than the minimum size,
// use the minimum size as our preferred size.
return minimumSize;
}
}
}
int[] computeSizes(int[] constraints, int availableSpace,
boolean computingRows) {
int[] result = computeMinSizes(constraints, computingRows);
int totalFixed = sumOfSizes(result);
int denominator = getResizeDenominator(computingRows);
int numRows = gridInfo.getNumRows(computingRows);
if (totalFixed < availableSpace) {
int remaining = availableSpace - totalFixed;
for (int idx = 0; idx < numRows && denominator > 0; idx++) {
Row row = getRow(idx, computingRows);
if (row.grows) {
int greed = row.size;
int amount = remaining * greed / denominator;
result[idx] += amount;
remaining -= amount;
denominator -= greed;
}
}
}
return result;
}
/**
* Computes one dimension of the preferred size of the layout.
*
* @param hint contains the result if already known, or SWT.DEFAULT if it needs to be computed
* @param constraints contains constraints along the other dimension, or SWT.DEFAULT if none. For
* example, if we are computing the preferred row sizes, this would be an array of known column sizes.
* @param computingRows if true, this method returns the height (pixels). Otherwise, it returns the
* width (pixels).
*/
int preferredSize(int[] constraints, boolean computingRows) {
int[] fixedSizes = computeMinSizes(constraints, computingRows);
return sumOfSizes(fixedSizes)
+ getDynamicSize(constraints, fixedSizes, computingRows);
}
/**
* Computes the sum of all integers in the given array. If any of the entries are SWT.DEFAULT,
* the result is SWT.DEFAULT.
*/
static int sumOfSizes(int[] input) {
return sumOfSizes(input, 0, input.length);
}
static int sumOfSizes(int[] input, int start, int length) {
int sum = 0;
for (int idx = start; idx < start + length; idx++) {
int next = input[idx];
if (next == SWT.DEFAULT) {
return SWT.DEFAULT;
}
sum += next;
}
return sum;
}
/**
* Returns the preferred dynamic width of the layout
*
* @param constraints
* @param fixedSizes
* @param computingRows
* @return
*/
int getDynamicSize(int[] constraints, int[] fixedSizes,
boolean computingRows) {
int result = 0;
int numerator = getResizeDenominator(computingRows);
// If no resizable columns, return
if (numerator == 0) {
return 0;
}
int rowSpacing = computingRows ? verticalSpacing : horizontalSpacing;
int colSpacing = computingRows ? horizontalSpacing : verticalSpacing;
int numControls = gridInfo.controls.length;
for (int idx = 0; idx < numControls; idx++) {
int controlRowStart = gridInfo.getStartPos(idx, computingRows);
int controlRowSpan = getSpan(idx, computingRows);
int controlColStart = gridInfo.getStartPos(idx, !computingRows);
int controlColSpan = getSpan(idx, !computingRows);
int denominator = getGrowthRatio(controlRowStart, controlRowSpan,
computingRows);
if (denominator > 0) {
int widthHint = sumOfSizes(constraints, controlColStart,
controlColSpan);
if (widthHint != SWT.DEFAULT) {
widthHint += colSpacing * (controlColSpan - 1);
}
// Compute the total control size
int controlSize = computeControlSize(idx, widthHint,
computingRows);
// Subtract the amount that overlaps fixed-size columns
controlSize -= sumOfSizes(fixedSizes, controlRowStart,
controlRowSpan);
// Subtract the amount that overlaps spacing between cells
controlSize -= (rowSpacing * (controlRowSpan - 1));
result = Math
.max(result, controlSize * numerator / denominator);
}
}
return result;
}
/**
* Computes one dimension of a control's size
*
* @param control the index of the control being computed
* @param constraint the other dimension of the control's size, or SWT.DEFAULT if unknown
* @param computingHeight if true, this method returns a height. Else it returns a width
* @return the preferred height or width of the control, in pixels
*/
int computeControlSize(int control, int constraint, boolean computingHeight) {
CellData data = gridInfo.getCellData(control);
// If we're looking for the preferred size of the control (without hints)
if (constraint == SWT.DEFAULT) {
Point result = data.computeSize(cache.getCache(control),
SWT.DEFAULT, SWT.DEFAULT);
// Return result
if (computingHeight) {
return result.y;
}
return result.x;
}
// Compute a height
if (computingHeight) {
return data.computeSize(cache.getCache(control), constraint,
SWT.DEFAULT).y;
}
return data.computeSize(cache.getCache(control), SWT.DEFAULT,
constraint).x;
}
/**
* Returns the relative amount that a control starting on the given row and spanning
* the given length will contribute
*
* @param start
* @param length
* @param computingRows
* @return
*/
int getGrowthRatio(int start, int length, boolean computingRows) {
boolean willGrow = false;
int sum = 0;
int end = start + length;
for (int idx = start; idx < end; idx++) {
Row row = getRow(idx, computingRows);
if (row.largerThanChildren && row.grows) {
willGrow = true;
}
sum += row.size;
}
if (!willGrow) {
return 0;
}
return sum;
}
int[] computeMinSizes(int[] constraints, boolean computingRows) {
// We cache the result of this function since it might be called more than once
// for a single size computation
int[] result = computingRows ? cachedRowMin : cachedColMin;
if (result == null) {
int columnSpacing;
int rowSpacing;
if (computingRows) {
columnSpacing = horizontalSpacing;
rowSpacing = verticalSpacing;
} else {
columnSpacing = verticalSpacing;
rowSpacing = horizontalSpacing;
}
int rowCount = gridInfo.getNumRows(computingRows);
result = new int[rowCount];
int colCount = gridInfo.getNumRows(!computingRows);
int[] rowControls = new int[colCount];
int lastGrowingRow = -1;
for (int idx = 0; idx < rowCount; idx++) {
Row row = getRow(idx, computingRows);
if (row.grows) {
// There is no minimum size for growing rows
lastGrowingRow = idx;
result[idx] = 0;
} else {
result[idx] = row.size;
if (row.largerThanChildren) {
// Determine which controls are in this row
gridInfo.getRow(rowControls, idx, computingRows);
for (int colIdx = 0; colIdx < rowControls.length; colIdx++) {
int control = rowControls[colIdx];
// The getRow method will insert -1 into empty cells... skip these.
if (control != -1) {
int controlStart = gridInfo.getStartPos(
control, computingRows);
int controlSpan = getSpan(control,
computingRows);
// If the control ends on this row and does not span any growing rows
if (controlStart + controlSpan - 1 == idx
&& controlStart > lastGrowingRow) {
int controlColStart = gridInfo.getStartPos(
control, !computingRows);
int controlColSpan = getSpan(control,
!computingRows);
int controlRowSpan = getSpan(control,
computingRows);
// Compute the width constraint for this control
int spannedWidth = sumOfSizes(constraints,
controlColStart, controlColSpan);
if (spannedWidth != SWT.DEFAULT) {
spannedWidth += (columnSpacing * (controlSpan - 1));
}
int controlHeight = computeControlSize(
control, spannedWidth,
computingRows);
// Determine how much of the control spans already allocated columns
int allocatedHeight = sumOfSizes(result,
controlColStart, controlRowSpan - 1)
+ (rowSpacing * (controlRowSpan - 1));
result[idx] = Math.max(result[idx],
controlHeight - allocatedHeight);
}
}
}
}
}
}
}
// Cache this result
if (computingRows) {
cachedRowMin = result;
} else {
cachedColMin = result;
}
return result;
}
/**
* Returns the height constraints that should be used when computing column widths. Requires initGrid
* to have been called first.
*
* @param result Will contain the height constraint for row i in the ith position of the array,
* or SWT.DEFAULT if there is no constraint on that row.
*/
private int[] computeConstraints(boolean horizontal) {
// Initialize the height constraints for each row (basically, these will always be SWT.DEFAULT,
// except for rows of type FixedRow, which have a constant height).
int numRows = gridInfo.getNumRows(horizontal);
int[] result = new int[numRows];
for (int idx = 0; idx < numRows; idx++) {
Row row = getRow(idx, horizontal);
if (!(row.grows || row.largerThanChildren)) {
result[idx] = row.size;
} else {
result[idx] = SWT.DEFAULT;
}
}
return result;
}
/**
* Computes the total greediness of all rows
*
* @return the total greediness of all rows
*/
private int getResizeDenominator(boolean horizontal) {
int result = 0;
int numRows = gridInfo.getNumRows(horizontal);
for (int idx = 0; idx < numRows; idx++) {
Row row = getRow(idx, horizontal);
if (row.grows) {
result += row.size;
}
}
return result;
}
// /**
// * Computes the total fixed height of all rows
// *
// * @param widthConstraints array where the nth entry indicates the known width of the
// * nth column, or SWT.DEFAULT if the width is still unknown
// *
// * @return the total fixed height for all rows
// */
// private int getMinimumSize(int[] constraints, boolean horizontal) {
// Control[] controls = new Control[gridInfo.getRows()];
// int result = 0;
// int numRows = gridInfo.getRows();
//
// for (int idx = 0; idx < numRows; idx++) {
// result += getRow(idx).getFixedHeight(gridInfo, widthConstraints, idx);
// }
//
// return result;
// }
protected int getSpan(int controlId, boolean isRow) {
CellData data = gridInfo.getCellData(controlId);
if (isRow) {
return data.verticalSpan;
}
return data.horizontalSpan;
}
/**
* Returns the total space that will be required for margins and spacing between and
* around cells. initGrid(...) must have been called first.
*
* @return
*/
private Point totalEmptySpace() {
int numRows = gridInfo.getRows();
return new Point((2 * marginWidth)
+ ((gridInfo.getCols() - 1) * horizontalSpacing),
(2 * marginHeight) + ((numRows - 1) * verticalSpacing));
}
/**
* Returns the absolute positions of each row, given the start position, row sizes,
* and row spacing
*
* @param startPos position of the initial row
* @param sizes array of row sizes (pixels)
* @param spacing space between each row (pixels)
* @return array of row positions. The result size is sizes.length + 1. The last entry is
* the position of the end of the layout.
*/
private static int[] computeRowPositions(int startPos, int[] sizes,
int spacing) {
int[] result = new int[sizes.length + 1];
result[0] = startPos;
for (int idx = 0; idx < sizes.length; idx++) {
result[idx + 1] = result[idx] + sizes[idx] + spacing;
}
return result;
}
/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Layout#layout(org.eclipse.swt.widgets.Composite, boolean)
*/
protected void layout(Composite composite, boolean flushCache) {
Control[] children = composite.getChildren();
// If there are no children then this is a NO-OP
if (children.length == 0)
return;
initGrid(children);
if (flushCache) {
cache.flush();
}
Point emptySpace = totalEmptySpace();
// Compute the area actually available for controls (once the margins and spacing is removed)
int availableWidth = composite.getClientArea().width - emptySpace.x;
int availableHeight = composite.getClientArea().height - emptySpace.y;
int[] heights = computeConstraints(true);
int[] widths = new int[gridInfo.getCols()];
// Compute the actual column widths
widths = computeSizes(heights, availableWidth, false);
// Compute the actual row heights (based on the actual column widths)
heights = computeSizes(widths, availableHeight, true);
Rectangle currentCell = new Rectangle(0, 0, 0, 0);
int[] starty = computeRowPositions(composite.getClientArea().y
+ marginHeight, heights, verticalSpacing);
int[] startx = computeRowPositions(composite.getClientArea().x
+ marginWidth, widths, horizontalSpacing);
int numChildren = gridInfo.controls.length;
for (int controlId = 0; controlId < numChildren; controlId++) {
CellData data = gridInfo.getCellData(controlId);
int row = gridInfo.controlRow[controlId];
int col = gridInfo.controlCol[controlId];
currentCell.x = startx[col];
currentCell.width = startx[col + data.horizontalSpan]
- currentCell.x - horizontalSpacing;
currentCell.y = starty[row];
currentCell.height = starty[row + data.verticalSpan]
- currentCell.y - verticalSpacing;
data.positionControl(cache.getCache(controlId), currentCell);
}
}
/**
* @return
*/
public int getColumns() {
return numCols;
}
public boolean canGrow(Composite composite, boolean horizontally) {
initGrid(composite.getChildren());
int numRows = gridInfo.getNumRows(horizontally);
for (int idx = 0; idx < numRows; idx++) {
Row row = getRow(idx, horizontally);
if (row.grows) {
return true;
}
}
return false;
}
}