/******************************************************************************* * Copyright (c) 2000, 2015 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.forms.widgets; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; 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; import org.eclipse.ui.internal.forms.widgets.FormUtil; /** * This implementation of the layout algorithm attempts to position controls in * the composite using a two-pass autolayout HTML table algorithm recommended by * HTML 4.01 W3C specification (see * http://www.w3.org/TR/html4/appendix/notes.html#h-B.5.2.2). The main * differences with GridLayout is that it has two passes and that width and * height are not calculated in the same pass. * <p> * The advantage of the algorithm over GridLayout is that it is capable of * flowing text controls capable of line wrap. These controls do not have * natural 'preferred size'. Instead, they are capable of providing the required * height if the width is set. Consequently, this algorithm first calculates the * widths that will be assigned to columns, and then passes those widths to the * controls to calculate the height. When a composite with this layout is a * child of the scrolling composite, they should interact in such a way that * reduction in the scrolling composite width results in the reflow and increase * of the overall height. * <p> * If none of the columns contain expandable and wrappable controls, the * end-result will be similar to the one provided by GridLayout. The difference * will show up for layouts that contain controls whose minimum and maximum * widths are not the same. * * @see TableWrapData * @since 3.0 */ public final class TableWrapLayout extends Layout implements ILayoutExtension { /** * Number of columns to use when positioning children (default is 1). */ public int numColumns = 1; /** * Left margin variable (default is 5). */ public int leftMargin = 5; /** * Right margin variable (default is 5). */ public int rightMargin = 5; /** * Top margin variable (default is 5). */ public int topMargin = 5; /** * Botom margin variable (default is 5). */ public int bottomMargin = 5; /** * Horizontal spacing (default is 5). */ public int horizontalSpacing = 5; /** * Vertical spacing (default is 5). */ public int verticalSpacing = 5; /** * If set to <code>true</code>, all the columns will have the same width. * Otherwise, column widths will be computed based on controls in them and * their layout data (default is <code>false</code>). */ public boolean makeColumnsEqualWidth = false; private boolean initialLayout = true; private Vector<TableWrapData[]> grid; private Hashtable<Control, RowSpan> rowspans; private int[] minColumnWidths, maxColumnWidths; private int widestColumnWidth; private int[] growingColumns; private int[] growingRows; private LayoutCache cache = new LayoutCache(); private class RowSpan { Control child; int row; int height; int totalHeight; public RowSpan(Control child, int column, int row) { this.child = child; this.row = row; } /* * Updates this row span's height with the given one if it is within * this span. */ public void update(int currentRow, int rowHeight) { TableWrapData td = (TableWrapData) child.getLayoutData(); // is currentRow within this span? if (currentRow >= row && currentRow < row + td.rowspan) { totalHeight += rowHeight; if (currentRow > row) totalHeight += verticalSpacing; } } public int getRequiredHeightIncrease() { if (totalHeight < height) return height - totalHeight; return 0; } } /** * Implements ILayoutExtension. Should not be called directly. * * @see ILayoutExtension */ @Override public int computeMinimumWidth(Composite parent, boolean changed) { Control[] children = parent.getChildren(); if (changed) { cache.flush(); } cache.setControls(children); changed = true; initializeIfNeeded(parent, changed); if (initialLayout) { changed = true; initialLayout = false; } if (grid == null || changed) { changed = true; grid = new Vector<>(); createGrid(parent); } if (minColumnWidths == null) minColumnWidths = new int[numColumns]; for (int i = 0; i < numColumns; i++) { minColumnWidths[i] = 0; } return internalGetMinimumWidth(parent, changed); } /** * Implements ILayoutExtension. Should not be called directly. * * @see ILayoutExtension */ @Override public int computeMaximumWidth(Composite parent, boolean changed) { Control[] children = parent.getChildren(); if (changed) { cache.flush(); } cache.setControls(children); changed = true; initializeIfNeeded(parent, changed); if (initialLayout) { changed = true; initialLayout = false; } if (grid == null || changed) { changed = true; grid = new Vector<>(); createGrid(parent); } if (maxColumnWidths == null) maxColumnWidths = new int[numColumns]; for (int i = 0; i < numColumns; i++) { maxColumnWidths[i] = 0; } return internalGetMaximumWidth(parent, changed); } /** * @see Layout#layout(Composite, boolean) */ @Override protected void layout(Composite parent, boolean changed) { Rectangle clientArea = parent.getClientArea(); Control[] children = parent.getChildren(); if (changed) { cache.flush(); } if (children.length == 0) return; cache.setControls(children); int parentWidth = clientArea.width; changed = true; initializeIfNeeded(parent, changed); if (initialLayout) { changed = true; initialLayout = false; } if (grid == null || changed) { changed = true; grid = new Vector<>(); createGrid(parent); } resetColumnWidths(); int minWidth = internalGetMinimumWidth(parent, changed); int maxWidth = internalGetMaximumWidth(parent, changed); int tableWidth = parentWidth; int[] columnWidths; if (parentWidth <= minWidth) { tableWidth = minWidth; if (makeColumnsEqualWidth) { columnWidths = new int[numColumns]; for (int i = 0; i < numColumns; i++) { columnWidths[i] = widestColumnWidth; } } else columnWidths = minColumnWidths; } else if (parentWidth > maxWidth) { if (growingColumns.length == 0) { tableWidth = maxWidth; columnWidths = maxColumnWidths; } else { columnWidths = new int[numColumns]; int extra = parentWidth - maxWidth; int colExtra = extra / growingColumns.length; for (int i = 0; i < numColumns; i++) { columnWidths[i] = maxColumnWidths[i]; if (isGrowingColumn(i)) { columnWidths[i] += colExtra; } } } } else { columnWidths = new int[numColumns]; if (makeColumnsEqualWidth) { int colSpace = tableWidth - leftMargin - rightMargin; colSpace -= (numColumns - 1) * horizontalSpacing; int col = colSpace / numColumns; for (int i = 0; i < numColumns; i++) { columnWidths[i] = col; } } else { columnWidths = assignExtraSpace(tableWidth, maxWidth, minWidth); } } int y = topMargin+clientArea.y; int[] rowHeights = computeRowHeights(children, columnWidths, changed); for (int i = 0; i < grid.size(); i++) { int rowHeight = rowHeights[i]; int x = leftMargin+clientArea.x; TableWrapData[] row = grid.elementAt(i); for (int j = 0; j < numColumns; j++) { TableWrapData td = row[j]; if (td.isItemData) { Control child = children[td.childIndex]; placeControl(child, td, x, y, rowHeights, i); } x += columnWidths[j]; if (j < numColumns - 1) x += horizontalSpacing; } y += rowHeight + verticalSpacing; } } int[] computeRowHeights(Control[] children, int[] columnWidths, boolean changed) { int[] rowHeights = new int[grid.size()]; for (int i = 0; i < grid.size(); i++) { TableWrapData[] row = grid.elementAt(i); rowHeights[i] = 0; for (int j = 0; j < numColumns; j++) { TableWrapData td = row[j]; if (td.isItemData == false) { continue; } Control child = children[td.childIndex]; int span = td.colspan; int cwidth = 0; for (int k = j; k < j + span; k++) { cwidth += columnWidths[k]; if (k < j + span - 1) cwidth += horizontalSpacing; } Point size = FormUtil.computeControlSize(cache.getCache(td.childIndex), cwidth - td.indent, td.maxWidth, td.maxHeight, td.align == TableWrapData.FILL); size.x += td.indent; td.compWidth = cwidth; if (td.heightHint != SWT.DEFAULT) { size = new Point(size.x, td.heightHint); } td.compSize = size; RowSpan rowspan = rowspans.get(child); if (rowspan == null) { rowHeights[i] = Math.max(rowHeights[i], size.y); } else rowspan.height = size.y; } updateRowSpans(i, rowHeights[i]); } for (Enumeration<RowSpan> enm = rowspans.elements(); enm.hasMoreElements();) { RowSpan rowspan = enm.nextElement(); int increase = rowspan.getRequiredHeightIncrease(); if (increase == 0) continue; TableWrapData td = (TableWrapData) rowspan.child.getLayoutData(); int ngrowing = 0; int[] affectedRows = new int[grid.size()]; for (int growingRow : growingRows) { if (growingRow >= rowspan.row && growingRow < rowspan.row + td.rowspan) { affectedRows[ngrowing++] = growingRow; } } if (ngrowing == 0) { ngrowing = 1; affectedRows[0] = rowspan.row + td.rowspan - 1; } increase += increase % ngrowing; int perRowIncrease = increase / ngrowing; for (int i = 0; i < ngrowing; i++) { int growingRow = affectedRows[i]; rowHeights[growingRow] += perRowIncrease; } } return rowHeights; } boolean isGrowingColumn(int col) { if (growingColumns == null) return false; for (int growingColumn : growingColumns) { if (col == growingColumn) return true; } return false; } int[] assignExtraSpace(int tableWidth, int maxWidth, int minWidth) { int fixedPart = leftMargin + rightMargin + (numColumns - 1) * horizontalSpacing; int D = maxWidth - minWidth; int W = tableWidth - minWidth; int widths[] = new int[numColumns]; int rem = 0; for (int i = 0; i < numColumns; i++) { int cmin = minColumnWidths[i]; int cmax = maxColumnWidths[i]; int d = cmax - cmin; int extra = D != 0 ? (d * W) / D : 0; if (i < numColumns - 1) { widths[i] = cmin + extra; rem += widths[i]; } else { widths[i] = tableWidth - fixedPart - rem; } } return widths; } void placeControl(Control control, TableWrapData td, int x, int y, int[] rowHeights, int row) { int xloc = x + td.indent; int yloc = y; int height = td.compSize.y; int colWidth = td.compWidth - td.indent; int width = td.compSize.x-td.indent; width = Math.min(width, colWidth); int slotHeight = rowHeights[row]; RowSpan rowspan = rowspans.get(control); if (rowspan != null) { slotHeight = 0; for (int i = row; i < row + td.rowspan; i++) { if (i > row) slotHeight += verticalSpacing; slotHeight += rowHeights[i]; } } // align horizontally if (td.align == TableWrapData.CENTER) { xloc = x + (colWidth - width) / 2; } else if (td.align == TableWrapData.RIGHT) { xloc = x + colWidth - width; } else if (td.align == TableWrapData.FILL) { width = colWidth; } // align vertically if (td.valign == TableWrapData.MIDDLE) { yloc = y + (slotHeight - height) / 2; } else if (td.valign == TableWrapData.BOTTOM) { yloc = y + slotHeight - height; } else if (td.valign == TableWrapData.FILL) { height = slotHeight; } control.setBounds(xloc, yloc, width, height); } void createGrid(Composite composite) { int row, column, rowFill, columnFill; Control[] children; TableWrapData spacerSpec; Vector<Integer> growingCols = new Vector<>(); Vector<Integer> growingRows = new Vector<>(); rowspans = new Hashtable<>(); // children = composite.getChildren(); if (children.length == 0) return; // grid.addElement(createEmptyRow()); row = 0; column = 0; // Loop through the children and place their associated layout specs in // the // grid. Placement occurs left to right, top to bottom (i.e., by row). for (int i = 0; i < children.length; i++) { // Find the first available spot in the grid. Control child = children[i]; TableWrapData spec = (TableWrapData) child.getLayoutData(); while (grid.elementAt(row)[column] != null) { column = column + 1; if (column >= numColumns) { row = row + 1; column = 0; if (row >= grid.size()) { grid.addElement(createEmptyRow()); } } } // See if the place will support the widget's horizontal span. If // not, go to the // next row. if (column + spec.colspan - 1 >= numColumns) { grid.addElement(createEmptyRow()); row = row + 1; column = 0; } // The vertical span for the item will be at least 1. If it is > 1, // add other rows to the grid. if (spec.rowspan > 1) { rowspans.put(child, new RowSpan(child, column, row)); } for (int j = 2; j <= spec.rowspan; j++) { if (row + j > grid.size()) { grid.addElement(createEmptyRow()); } } // Store the layout spec. Also cache the childIndex. NOTE: That we // assume the children of a // composite are maintained in the order in which they are created // and added to the composite. grid.elementAt(row)[column] = spec; spec.childIndex = i; if (spec.grabHorizontal) { updateGrowingColumns(growingCols, spec, column); } if (spec.grabVertical) { updateGrowingRows(growingRows, spec, row); } // Put spacers in the grid to account for the item's vertical and // horizontal // span. rowFill = spec.rowspan - 1; columnFill = spec.colspan - 1; for (int r = 1; r <= rowFill; r++) { for (int c = 0; c < spec.colspan; c++) { spacerSpec = new TableWrapData(); spacerSpec.isItemData = false; grid.elementAt(row + r)[column + c] = spacerSpec; } } for (int c = 1; c <= columnFill; c++) { for (int r = 0; r < spec.rowspan; r++) { spacerSpec = new TableWrapData(); spacerSpec.isItemData = false; grid.elementAt(row + r)[column + c] = spacerSpec; } } column = column + spec.colspan - 1; } // Fill out empty grid cells with spacers. for (int k = column + 1; k < numColumns; k++) { spacerSpec = new TableWrapData(); spacerSpec.isItemData = false; grid.elementAt(row)[k] = spacerSpec; } for (int k = row + 1; k < grid.size(); k++) { spacerSpec = new TableWrapData(); spacerSpec.isItemData = false; grid.elementAt(k)[column] = spacerSpec; } growingColumns = new int[growingCols.size()]; for (int i = 0; i < growingCols.size(); i++) { growingColumns[i] = growingCols.get(i).intValue(); } this.growingRows = new int[growingRows.size()]; for (int i = 0; i < growingRows.size(); i++) { this.growingRows[i] = growingRows.get(i).intValue(); } } private void updateGrowingColumns(Vector<Integer> growingColumns, TableWrapData spec, int column) { int affectedColumn = column + spec.colspan - 1; for (int i = 0; i < growingColumns.size(); i++) { Integer col = growingColumns.get(i); if (col.intValue() == affectedColumn) return; } growingColumns.add(Integer.valueOf(affectedColumn)); } private void updateGrowingRows(Vector<Integer> growingRows, TableWrapData spec, int row) { int affectedRow = row + spec.rowspan - 1; for (int i = 0; i < growingRows.size(); i++) { Integer irow = growingRows.get(i); if (irow.intValue() == affectedRow) return; } growingRows.add(Integer.valueOf(affectedRow)); } private TableWrapData[] createEmptyRow() { TableWrapData[] row = new TableWrapData[numColumns]; for (int i = 0; i < numColumns; i++) row[i] = null; return row; } /** * @see Layout#computeSize(Composite, int, int, boolean) */ @Override protected Point computeSize(Composite parent, int wHint, int hHint, boolean changed) { Control[] children = parent.getChildren(); if (changed) { cache.flush(); } if (children.length == 0) { return new Point(0, 0); } cache.setControls(children); int parentWidth = wHint; changed = true; initializeIfNeeded(parent, changed); if (initialLayout) { changed = true; initialLayout = false; } if (grid == null || changed) { changed = true; grid = new Vector<>(); createGrid(parent); } resetColumnWidths(); int minWidth = internalGetMinimumWidth(parent, changed); int maxWidth = internalGetMaximumWidth(parent, changed); if (wHint == SWT.DEFAULT) parentWidth = maxWidth; int tableWidth = parentWidth; int[] columnWidths; if (parentWidth <= minWidth) { tableWidth = minWidth; if (makeColumnsEqualWidth) { columnWidths = new int[numColumns]; for (int i = 0; i < numColumns; i++) { columnWidths[i] = widestColumnWidth; } } else columnWidths = minColumnWidths; } else if (parentWidth >= maxWidth) { if (makeColumnsEqualWidth) { columnWidths = new int[numColumns]; int colSpace = parentWidth - leftMargin - rightMargin; colSpace -= (numColumns - 1) * horizontalSpacing; int col = colSpace / numColumns; for (int i = 0; i < numColumns; i++) { columnWidths[i] = col; } } else { tableWidth = maxWidth; columnWidths = maxColumnWidths; } } else { columnWidths = new int[numColumns]; if (makeColumnsEqualWidth) { int colSpace = tableWidth - leftMargin - rightMargin; colSpace -= (numColumns - 1) * horizontalSpacing; int col = colSpace / numColumns; for (int i = 0; i < numColumns; i++) { columnWidths[i] = col; } } else { columnWidths = assignExtraSpace(tableWidth, maxWidth, minWidth); } } int totalHeight = 0; int innerHeight = 0; // compute widths for (int i = 0; i < grid.size(); i++) { TableWrapData[] row = grid.elementAt(i); // assign widths, calculate heights int rowHeight = 0; for (int j = 0; j < numColumns; j++) { TableWrapData td = row[j]; if (td.isItemData == false) { continue; } Control child = children[td.childIndex]; int span = td.colspan; int cwidth = 0; for (int k = j; k < j + span; k++) { if (k > j) cwidth += horizontalSpacing; cwidth += columnWidths[k]; } int cy = td.heightHint; if (cy == SWT.DEFAULT) { SizeCache controlCache = cache.getCache(td.childIndex); Point size = FormUtil.computeControlSize(controlCache, cwidth - td.indent, td.maxWidth, td.maxHeight, td.align == TableWrapData.FILL); cy = size.y; } RowSpan rowspan = rowspans.get(child); if (rowspan != null) { // don't take the height of this child into acount // because it spans multiple rows rowspan.height = cy; } else { rowHeight = Math.max(rowHeight, cy); } } updateRowSpans(i, rowHeight); if (i > 0) innerHeight += verticalSpacing; innerHeight += rowHeight; } if (!rowspans.isEmpty()) innerHeight = compensateForRowSpans(innerHeight); totalHeight = topMargin + innerHeight + bottomMargin; return new Point(tableWidth, totalHeight); } private void updateRowSpans(int row, int rowHeight) { if (rowspans == null || rowspans.size() == 0) return; for (Enumeration<RowSpan> enm = rowspans.elements(); enm.hasMoreElements();) { RowSpan rowspan = enm.nextElement(); rowspan.update(row, rowHeight); } } private int compensateForRowSpans(int totalHeight) { for (Enumeration<RowSpan> enm = rowspans.elements(); enm.hasMoreElements();) { RowSpan rowspan = enm.nextElement(); totalHeight += rowspan.getRequiredHeightIncrease(); } return totalHeight; } int internalGetMinimumWidth(Composite parent, boolean changed) { if (changed) //calculateMinimumColumnWidths(parent, true); calculateColumnWidths(parent, minColumnWidths, false, true, makeColumnsEqualWidth); int minimumWidth = 0; widestColumnWidth = 0; if (makeColumnsEqualWidth) { for (int i = 0; i < numColumns; i++) { widestColumnWidth = Math.max(widestColumnWidth, minColumnWidths[i]); } } for (int i = 0; i < numColumns; i++) { if (i > 0) minimumWidth += horizontalSpacing; if (makeColumnsEqualWidth) minimumWidth += widestColumnWidth; else minimumWidth += minColumnWidths[i]; } // add margins minimumWidth += leftMargin + rightMargin; return minimumWidth; } int internalGetMaximumWidth(Composite parent, boolean changed) { if (changed) //calculateMaximumColumnWidths(parent, true); calculateColumnWidths(parent, maxColumnWidths, true, true, makeColumnsEqualWidth); int maximumWidth = 0; for (int i = 0; i < numColumns; i++) { if (i > 0) maximumWidth += horizontalSpacing; maximumWidth += maxColumnWidths[i]; } // add margins maximumWidth += leftMargin + rightMargin; return maximumWidth; } void resetColumnWidths() { if (minColumnWidths == null) minColumnWidths = new int[numColumns]; if (maxColumnWidths == null) maxColumnWidths = new int[numColumns]; for (int i = 0; i < numColumns; i++) { minColumnWidths[i] = 0; } for (int i = 0; i < numColumns; i++) { maxColumnWidths[i] = 0; } } void calculateColumnWidths(Composite parent, int [] columnWidths, boolean max, boolean changed, boolean makeColumnsEqualWidth2) { boolean secondPassNeeded=false; int widestColumnWidth = 0; for (int i = 0; i < grid.size(); i++) { TableWrapData[] row = grid.elementAt(i); for (int j = 0; j < numColumns; j++) { TableWrapData td = row[j]; if (td.isItemData == false) continue; if (td.colspan>1) { // we will not do controls with multiple column span // here - increment and continue secondPassNeeded=true; j+=td.colspan-1; continue; } SizeCache childCache = cache.getCache(td.childIndex); // !! int width = max?childCache.computeMaximumWidth():childCache.computeMinimumWidth(); if (td.maxWidth!=SWT.DEFAULT) width = Math.min(width, td.maxWidth); width += td.indent; columnWidths[j] = Math.max(columnWidths[j], width); widestColumnWidth = Math.max(widestColumnWidth, columnWidths[j]); } } if (makeColumnsEqualWidth) { for (int i = 0; i < numColumns; i++) { columnWidths[i] = widestColumnWidth; } } if (!secondPassNeeded) return; // Second pass for controls with multi-column horizontal span for (int i = 0; i < grid.size(); i++) { TableWrapData[] row = grid.elementAt(i); for (int j = 0; j < numColumns; j++) { TableWrapData td = row[j]; if (td.isItemData == false || td.colspan==1) continue; SizeCache childCache = cache.getCache(td.childIndex); int width = max?childCache.computeMaximumWidth():childCache.computeMinimumWidth(); if (td.maxWidth!=SWT.DEFAULT) width = Math.min(width, td.maxWidth); width += td.indent; // check if the current width is enough to // support the control; if not, add the delta to // the last column or to all the growing columns, if present int current = 0; for (int k = j; k < j + td.colspan; k++) { if (k > j) current += horizontalSpacing; current += columnWidths[k]; } if (width <= current) { // we are ok - nothing to do here } else { int ndiv = 0; if (growingColumns != null) { for (int k = j; k < j + td.colspan; k++) { if (isGrowingColumn(k)) { ndiv++; } } } if (ndiv == 0) { // add the delta to the last column columnWidths[j + td.colspan - 1] += width - current; } else { // distribute the delta to the growing // columns int percolumn = (width - current) / ndiv; if ((width - current) % ndiv > 0) percolumn++; for (int k = j; k < j + td.colspan; k++) { if (isGrowingColumn(k)) columnWidths[k] += percolumn; } } } } } } boolean isWrap(Control control) { if (control instanceof Composite && ((Composite) control).getLayout() instanceof ILayoutExtension) return true; return (control.getStyle() & SWT.WRAP) != 0; } private void initializeIfNeeded(Composite parent, boolean changed) { if (changed) initialLayout = true; if (initialLayout) { initializeLayoutData(parent); initialLayout = false; } } void initializeLayoutData(Composite composite) { Control[] children = composite.getChildren(); for (Control child : children) { if (child.getLayoutData() == null) { child.setLayoutData(new TableWrapData()); } } } }