package org.eclipse.swt.nebula.widgets.compositetable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Widget; class HeaderLayout extends AbstractGridRowLayout { private static final int MINIMUM_COL_WIDTH = 5; private boolean layingOut = false; private int[] lastWidths = null; private AbstractNativeHeader header = null; /** * Constructor HeaderLayout. The default constructor. If you use this * constructor, you must manually specify the column weights, and possibly, * the fittingHorizontally property value. */ public HeaderLayout() { super(); } /** * Constructor HeaderLayout. Construct a HeaderLayout, specifying the * column weights. By default, fittingHorizontally is false. * * @param weights * int[] The amount of weight desired for each column in the * table. If fittingHorizontally is set to true, the sum of all * weights must be 100 and each weight indicates the percentage * of the whole table that each column will occupy. If * fittingHorizontally is set to false, each weight is the * minimum width of the column in pixels. If the table is * narrower than can fit all widths, CompositeTable will display * a horizontal scroll bar. If the table is wider than can fit * all widths, the columns are scaled so that the entire table * fills the desired space and the ratios of the column widths * remains constant. fittingHorizontally defaults to false. */ public HeaderLayout(int[] weights) { super(weights); } /** * Construct a HeaderLayout, specifying both the weights and the * fittingHorizontally property. * * @param weights * int[] The amount of weight desired for each column in the * table. If fittingHorizontally is set to true, the sum of all * weights must be 100 and each weight indicates the percentage * of the whole table that each column will occupy. If * fittingHorizontally is set to false, each weight is the * minimum width of the column in pixels. If the table is * narrower than can fit all widths, CompositeTable will display * a horizontal scroll bar. If the table is wider than all * minimum column widths, the columns will be scaled so that the * ratios of the actual widths remains constant and all columns * fit exactly in the available space. fittingHorizontally * defaults to false. * * @param fittingHorizontally * If true, the weights are interpreted as percentages and the * column widths are scaled so that each column occupies the * percentage of the total width indicated by its weight. If * false, the weights are interpreted as minimum column widths. * If the table is narrower than can accommodate those widths, * CompositeTable will display a horizontal scroll bar. If the * table is wider than all minimum column widths, the columns * will be scaled so that the ratios of the actual widths remains * constant and all columns fit exactly in the available space. */ public HeaderLayout(int[] weights, boolean fittingHorizontally) { super(weights, fittingHorizontally); } protected Point computeSize(Composite child, int wHint, int hHint, boolean flushCache) { storeHeader(child); return super.computeSize(child, wHint, hHint, flushCache); } private void storeHeader(Composite child) { this.header = getHeader(child); } /* * FEATURE in SWT/Win32: When resizing a Shell larger, we get two layout * events from SWT. (1) where the Shell has gotten bigger but the child * hasn't; (2) where both the Shell and child have gotten bigger. When * the Shell is getting bigger, we want to only process events under (2). * * The problem is that if the headerTable's contents is made larger before * the child is, the headerTable will display scroll bar(s), which will * then be erased when event (2) arrives. The solution is to only process * event (2). * * NEBULA_TODO: Test on Linux/GTK. */ private int lastChildWidth = -1; private int lastShellWidth = -1; protected void layout(final Composite child, boolean flushCache) { storeHeader(child); int childWidth = child.getSize().x; int shellWidth = child.getShell().getSize().x; if (childWidth == lastChildWidth && shellWidth > lastShellWidth) return; if (childWidth > lastChildWidth) { final Table headerTable = getHeader(child).headerTable; headerTable.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { headerTable.removePaintListener(this); layout(child); } }); } else { layout(child); } lastChildWidth = childWidth; lastShellWidth = shellWidth; } private void layout(Composite child) { layingOut = true; try { super.layout(child, true); storeLastWidths(getHeader(child).headerTable); } finally { layingOut = false; } } protected void storeLastWidths(Table table) { if (lastWidths == null) { lastWidths = new int[table.getColumnCount()]; } TableColumn[] columns = table.getColumns(); for (int col = 0; col < columns.length; col++) { lastWidths[col] = columns[col].getWidth(); } } // Inherited from AbstractGridRowLayout ------------------------------------ public int[] getWeights() { if (header == null) { return super.getWeights(); } else { int[] weightsOrder = header.headerTable.getColumnOrder(); int[] rawWeights = super.getWeights(); int[] orderedWeights = new int[weightsOrder.length]; for (int i = 0; i < orderedWeights.length; i++) { orderedWeights[i] = rawWeights[weightsOrder[i]]; } return orderedWeights; } } public AbstractGridRowLayout setWeights(int[] weights) { if (header == null) { super.setWeights(weights); } else { int[] weightsOrder = header.headerTable.getColumnOrder(); int[] orderedWeights = new int[weightsOrder.length]; for (int i = 0; i < orderedWeights.length; i++) { orderedWeights[weightsOrder[i]] = weights[i]; super.setWeights(orderedWeights); } } return this; } protected int computeMaxHeight(Composite rowOrHeader) { return getHeader(rowOrHeader).headerTable.getHeaderHeight(); } protected Point computeColumnSize(Widget columnObject, int wHint, int hHint, boolean flush) { TableColumn tableColumn = (TableColumn) columnObject; int currentWidth = tableColumn.getWidth(); int headerHeight = tableColumn.getParent().getHeaderHeight(); return new Point(currentWidth, headerHeight); } protected Widget getColumnAt(Composite rowOrHeader, int offset) { Table headerTable = getHeader(rowOrHeader).headerTable; int[] columnOrder = headerTable.getColumnOrder(); return headerTable.getColumn(columnOrder[offset]); } protected int getNumColumns(Composite rowOrHeader) { return getHeader(rowOrHeader).headerTable.getColumnCount(); } protected void setBounds(Widget columnObject, int left, int top, int width, int height) { TableColumn tableColumn = (TableColumn) columnObject; tableColumn.setWidth(width + 2*CELL_BORDER_WIDTH); } // Utility methods --------------------------------------------------------- private AbstractNativeHeader asHeader(Widget rowOrHeader) { if (!(rowOrHeader instanceof AbstractNativeHeader)) { throw new IllegalArgumentException("HeaderLayout must be used on an AbstractHeader"); } return (AbstractNativeHeader) rowOrHeader; } private AbstractNativeHeader getHeader(Widget rowOrHeader) { AbstractNativeHeader header = asHeader(rowOrHeader); if (headerControlListener == null) { headerControlListener = new HeaderControlListener(); header.addColumnControlListener(headerControlListener); header.addDisposeListener(headerDisposeListener); } return header; } private boolean resizedColumnIsNotTheLastColumn(int resizedColumnNumber, Table table) { return resizedColumnNumber < table.getColumnCount()-1; } private int computePercentage(int totalAvailableWidth, int columnWidth) { return (int) (((double) columnWidth) / totalAvailableWidth * 100); } // Called from event handlers ---------------------------------------------- private void adjustWeights(AbstractNativeHeader header, TableColumn resizedColumn) { int totalAvailableWidth = getAvailableWidth(header); int resizedColumnNumber = 0; int newTotalWidth = 0; TableColumn[] columns = resizedColumn.getParent().getColumns(); for (int i = 0; i < columns.length; i++) { newTotalWidth += columns[i].getWidth(); if (columns[i] == resizedColumn) { resizedColumnNumber = i; } } Table table = resizedColumn.getParent(); int[] columnOrder = table.getColumnOrder(); int resizedColumnPosition = 0; for (int i = 0; i < columnOrder.length; i++) { if (columnOrder[i] == resizedColumnNumber) { resizedColumnPosition = i; break; } } if (resizedColumnIsNotTheLastColumn(resizedColumnPosition, resizedColumn.getParent())) { // Compute resized column width change and make sure the resized // column's width is sane int resizedColumnWidth = resizedColumn.getWidth(); // int columnWidthChange = lastWidths[resizedColumnPosition] - resizedColumnWidth; int columnWidthChange = lastWidths[columnOrder[resizedColumnPosition]] - resizedColumnWidth; int columnWidthChangeTooFar = MINIMUM_COL_WIDTH - resizedColumnWidth; if (columnWidthChangeTooFar > 0) { columnWidthChange -= columnWidthChangeTooFar; resizedColumnWidth = MINIMUM_COL_WIDTH; resizedColumn.setWidth(resizedColumnWidth); } // Fix the width of the column to the right of the resized column int columnToTheRightOfResizedColumnWidth = lastWidths[columnOrder[resizedColumnPosition+1]] + columnWidthChange; // int columnToTheRightOfResizedColumnWidth = // lastWidths[resizedColumnPosition+1] + columnWidthChange; columnWidthChangeTooFar = MINIMUM_COL_WIDTH - columnToTheRightOfResizedColumnWidth; if (columnWidthChangeTooFar > 0) { columnWidthChange += columnWidthChangeTooFar; resizedColumnWidth -= columnWidthChangeTooFar; resizedColumn.setWidth(resizedColumnWidth); columnToTheRightOfResizedColumnWidth = MINIMUM_COL_WIDTH; } TableColumn columnToTheRightOfResizedColumn = columns[columnOrder[resizedColumnPosition+1]]; columnToTheRightOfResizedColumn.setWidth(columnToTheRightOfResizedColumnWidth); if (isFittingHorizontally()) { adjustWeightedHeader(header, resizedColumnPosition, resizedColumn, columnToTheRightOfResizedColumn, totalAvailableWidth, newTotalWidth); } else { // Fix the weights based on if the column sizes are being scaled if (isWidthWiderThanAllColumns(header)) { adjustScaledAbsoluteWidthWeights(resizedColumnPosition, resizedColumnWidth, columnToTheRightOfResizedColumnWidth, header.getSize().x); } else { adjustNonScaledAbsoluteWidthWeights(resizedColumnPosition, resizedColumnWidth, columnToTheRightOfResizedColumnWidth); } } fireColumnResizedEvent(resizedColumnPosition, resizedColumnWidth, columnToTheRightOfResizedColumnWidth); } else { // Re-layout; the rightmost column can't be resized layout(header, true); } } private List columnControlListeners = new ArrayList(); void addColumnControlListener(ColumnControlListener l) { columnControlListeners.add(l); } void removeColumnControlListener(ColumnControlListener l) { columnControlListeners.remove(l); } private void fireColumnResizedEvent(int resizedColumnNumber, int resizedColumnWidth, int columnToTheRightOfResizedColumnWidth) { for (Iterator i = columnControlListeners.iterator(); i.hasNext();) { ColumnControlListener l = (ColumnControlListener) i.next(); l.columnResized(resizedColumnNumber, resizedColumnWidth, columnToTheRightOfResizedColumnWidth); } } private void fireColumnMovedEvent(int[] newColumnOrder) { for (Iterator i = columnControlListeners.iterator(); i.hasNext();) { ColumnControlListener l = (ColumnControlListener) i.next(); l.columnMoved(newColumnOrder); } } private void adjustWeightedHeader(AbstractNativeHeader header, int resizedColumnPosition, TableColumn resizedColumn, TableColumn columnToTheRightOfResizedColumn, int totalAvailableWidth, int newTotalWidth) { // Adjust percentage of the resized column and the column to the right int[] weights = getWeights(); int resizedColumnPercentage = computePercentage(totalAvailableWidth, resizedColumn.getWidth()); weights[resizedColumnPosition] = resizedColumnPercentage; giveRemainderToWeightAtColumn(resizedColumnPosition+1); setWeights(weights); } private void adjustScaledAbsoluteWidthWeights(int resizedColumnPosition, int resizedColumnWidth, int columnToTheRightOfResizedColumnWidth, int headerWidth) { int sumOfAllWeights = getSumOfAllWeights(); double scalingFactor = (double)headerWidth / (double) sumOfAllWeights; int unscaledResizedColumWidth = (int)(resizedColumnWidth / scalingFactor); int unscaledColumnToTheRightOfResizedColumnWidth = (int)(columnToTheRightOfResizedColumnWidth / scalingFactor); adjustNonScaledAbsoluteWidthWeights(resizedColumnPosition, unscaledResizedColumWidth, unscaledColumnToTheRightOfResizedColumnWidth); } private void adjustNonScaledAbsoluteWidthWeights(int resizedColumnPosition, int resizedColumnWidth, int columnToTheRightOfResizedColumnWidth) { int[] weights = getWeights(); int oldWeightSum = getSumOfAllWeights(); int currentWeightSum = oldWeightSum; currentWeightSum += weights[resizedColumnPosition] - resizedColumnWidth; currentWeightSum += weights[resizedColumnPosition+1] - columnToTheRightOfResizedColumnWidth; int weightSumChange = oldWeightSum - currentWeightSum; columnToTheRightOfResizedColumnWidth -= weightSumChange; weights[resizedColumnPosition] = resizedColumnWidth; weights[resizedColumnPosition+1] = columnToTheRightOfResizedColumnWidth; setWeights(weights); } private void giveRemainderToWeightAtColumn(int columnNumber) { int[] weights = getWeights(); int totalWeightPercentage = 0; for (int i = 0; i < weights.length; i++) { totalWeightPercentage += weights[i]; } int spareWidthPercentage = 100 - totalWeightPercentage; weights[columnNumber] += spareWidthPercentage; } // Event listeners -------------------------------------------------------- private ControlListener headerControlListener = null; private boolean wasResized = true; private class HeaderControlListener implements ControlListener { public void controlMoved(ControlEvent e) { // Eat the move event that is fired after resize events if (wasResized) { wasResized = false; return; } Table table = header.headerTable; fireColumnMovedEvent(table.getColumnOrder()); storeLastWidths(table); } public void controlResized(ControlEvent e) { if (lastWidths == null) return; wasResized = true; if (!layingOut) { layingOut = true; try { TableColumn tableColumn = (TableColumn) e.widget; AbstractNativeHeader header = asHeader(tableColumn.getParent().getParent()); adjustWeights(header, tableColumn); storeLastWidths(tableColumn.getParent()); } finally { layingOut = false; } } } } private DisposeListener headerDisposeListener = new DisposeListener() { public void widgetDisposed(DisposeEvent e) { asHeader(e.widget).removeColumnControlListener(headerControlListener); } }; }