/*******************************************************************************
* Copyright (c) 2006-2013 The RCP Company 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:
* The RCP Company - initial API and implementation
*******************************************************************************/
package com.rcpcompany.uibindings.internal.utils;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.util.Policy;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.ColumnLayoutData;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.ColumnWeightData;
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.Event;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Widget;
import com.rcpcompany.utils.logging.LogUtils;
/**
* This {@link Layout} is used to set the size of a table in a consistent way.
*/
public abstract class UITableLayout extends Layout {
private static int COLUMN_TRIM;
static {
if (Util.isWindows()) {
COLUMN_TRIM = 4;
} else if (Util.isMac()) {
COLUMN_TRIM = 24;
} else {
COLUMN_TRIM = 3;
}
}
static final boolean IS_GTK = Util.isGtk();
/**
* Key used to restore the layout data in the columns data-slot
*/
protected static final String LAYOUT_DATA = Policy.JFACE + ".LAYOUT_DATA"; //$NON-NLS-1$
private boolean inupdateMode = false;
private boolean relayout = true;
private final Listener resizeListener = new Listener() {
@Override
public void handleEvent(Event event) {
if (!inupdateMode) {
updateColumnData(event.widget);
}
}
};
private LayoutRunner myLayoutRunnable;
/**
* Adds a new column of data to this table layout.
*
* @param column the column
*
* @param data the column layout data
*/
public void setColumnData(Widget column, ColumnLayoutData data) {
if (column.getData(LAYOUT_DATA) == null) {
column.addListener(SWT.Resize, resizeListener);
}
column.setData(LAYOUT_DATA, data);
}
/**
* Compute the size of the table or tree based on the ColumnLayoutData and the width and height
* hint.
*
* @param scrollable the widget to compute
* @param wHint the width hint
* @param hHint the height hint
* @return Point where x is the width and y is the height
*/
private Point computeTableTreeSize(Scrollable scrollable, int wHint, int hHint) {
final Point result = scrollable.computeSize(wHint, hHint);
int width = 0;
final int size = getColumnCount(scrollable);
for (int i = 0; i < size; ++i) {
final ColumnLayoutData layoutData = getLayoutData(scrollable, i);
if (layoutData instanceof ColumnPixelData) {
final ColumnPixelData col = (ColumnPixelData) layoutData;
width += col.width;
if (col.addTrim) {
width += getColumnTrim();
}
} else if (layoutData instanceof ColumnWeightData) {
final ColumnWeightData col = (ColumnWeightData) layoutData;
width += col.minimumWidth;
} else {
Assert.isTrue(false, "Unknown column layout data"); //$NON-NLS-1$
}
}
if (width > result.x) {
result.x = width;
}
return result;
}
/**
* Layout the scrollable based on the supplied width and area. Only increase the size of the
* scrollable if increase is <code>true</code>.
*
* @param scrollable
* @param width
* @param area
* @param increase
*/
private void layoutTableTree(final Scrollable scrollable, final int width, final Rectangle area,
final boolean increase) {
final int numberOfColumns = getColumnCount(scrollable);
final int[] widths = new int[numberOfColumns];
final int[] weightColumnIndices = new int[numberOfColumns];
int numberOfWeightColumns = 0;
int fixedWidth = 0;
int totalWeight = 0;
// First calc space occupied by fixed columns
for (int i = 0; i < numberOfColumns; i++) {
final ColumnLayoutData col = getLayoutData(scrollable, i);
if (col instanceof ColumnPixelData) {
final ColumnPixelData cpd = (ColumnPixelData) col;
int pixels = cpd.width;
if (cpd.addTrim) {
pixels += getColumnTrim();
}
widths[i] = pixels;
fixedWidth += pixels;
} else if (col instanceof ColumnWeightData) {
final ColumnWeightData cw = (ColumnWeightData) col;
weightColumnIndices[numberOfWeightColumns] = i;
numberOfWeightColumns++;
totalWeight += cw.weight;
} else {
Assert.isTrue(false, "Unknown column layout data"); //$NON-NLS-1$
}
}
boolean recalculate;
do {
recalculate = false;
for (int i = 0; i < numberOfWeightColumns; i++) {
final int colIndex = weightColumnIndices[i];
final ColumnWeightData cw = (ColumnWeightData) getLayoutData(scrollable, colIndex);
final int minWidth = cw.minimumWidth;
final int allowedWidth = totalWeight == 0 ? 0 : (width - fixedWidth) * cw.weight / totalWeight;
if (allowedWidth < minWidth) {
/*
* if the width assigned by weight is less than the minimum, then treat this
* column as fixed, remove it from weight calculations, and recalculate other
* weights.
*/
numberOfWeightColumns--;
totalWeight -= cw.weight;
fixedWidth += minWidth;
widths[colIndex] = minWidth;
System.arraycopy(weightColumnIndices, i + 1, weightColumnIndices, i, numberOfWeightColumns - i);
recalculate = true;
break;
}
widths[colIndex] = allowedWidth;
}
} while (recalculate);
if (increase) {
scrollable.setSize(area.width, area.height);
}
inupdateMode = true;
int wsum = 0;
for (final int w : widths) {
wsum += w;
}
// LogUtils.debug(this, "sum=" + wsum + ", widths=" + Arrays.toString(widths));
setColumnWidths(scrollable, widths);
scrollable.update();
inupdateMode = false;
if (!increase) {
scrollable.setSize(area.width, area.height);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Layout#computeSize(org.eclipse.swt.widgets.Composite , int, int,
* boolean)
*/
@Override
protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
return computeTableTreeSize(getControl(composite), wHint, hHint);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Layout#layout(org.eclipse.swt.widgets.Composite, boolean)
*/
@Override
protected void layout(Composite composite, boolean flushCache) {
if (myLayoutRunnable != null) {
myLayoutRunnable.cancelled = true;
myLayoutRunnable = null;
}
myLayoutRunnable = new LayoutRunner(composite, flushCache);
composite.getDisplay().timerExec(100, myLayoutRunnable);
}
private class LayoutRunner implements Runnable {
public boolean cancelled = false;
private final Composite myComposite;
private final boolean myFlushCache;
private LayoutRunner(Composite composite, boolean flushCache) {
myComposite = composite;
myFlushCache = flushCache;
}
@Override
public void run() {
if (cancelled) return;
if (myComposite.isDisposed()) return;
doLayout(myComposite, myFlushCache);
}
}
protected void doLayout(Composite composite, boolean flushCache) {
final Rectangle area = composite.getClientArea();
final Scrollable table = getControl(composite);
final int tableWidth = table.getSize().x;
final int trim = computeTrim(area, table, tableWidth);
final int width = Math.max(0, area.width - trim);
LogUtils.debug(this, "clientArea=" + area + ", tableWidth=" + tableWidth + ", trim=" + trim + ", width="
+ width);
if (width > 1) {
layoutTableTree(table, width, area, tableWidth < area.width);
}
// For the first time we need to relayout because Scrollbars are not
// calculate appropriately
if (relayout) {
relayout = false;
composite.layout();
}
}
/**
* Compute the area required for trim.
*
* @param area
* @param scrollable
* @param currentWidth
* @return int
*/
private int computeTrim(Rectangle area, Scrollable scrollable, int currentWidth) {
int trim;
if (currentWidth > 1) {
trim = currentWidth - scrollable.getClientArea().width;
} else {
// initially, the table has no extend and no client area - use the
// border with
// plus some padding as educated guess
trim = 2 * scrollable.getBorderWidth() + 1;
}
return trim;
}
/**
* Get the control being laid out.
*
* @param composite the composite with the layout
* @return {@link Scrollable}
*/
Scrollable getControl(Composite composite) {
return (Scrollable) composite.getChildren()[0];
}
/**
* Get the number of columns for the receiver.
*
* @param tableTree the control
*
* @return the number of columns
*/
protected abstract int getColumnCount(Scrollable tableTree);
/**
* Set the widths of the columns.
*
* @param tableTree the control
*
* @param widths the widths of the column
*/
protected abstract void setColumnWidths(Scrollable tableTree, int[] widths);
/**
* Get the layout data for a column
*
* @param tableTree the control
* @param columnIndex the column index
* @return the layout data, might <b>not</b> null
*/
protected abstract ColumnLayoutData getLayoutData(Scrollable tableTree, int columnIndex);
/**
* Update the layout data for a column
*
* @param column the column
*/
protected abstract void updateColumnData(Widget column);
/**
* The number of extra pixels taken as horizontal trim by the table column. To ensure there are
* N pixels available for the content of the column, assign N+COLUMN_TRIM for the column width.
*
* @return the trim used by the columns
*/
protected int getColumnTrim() {
return COLUMN_TRIM;
}
}