blob: 1704c55f1465bfcd1c0b7e560e770851d13b1883 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.layout;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.util.Geometry;
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;
/**
* Lays out the children of a Composite. One control occupies the center of the composite,
* and any number of controls may be attached to the top, bottom, and sides. This is a
* common layout for application windows, which typically have a central work area
* and a number of small widgets (toolbars, status lines, etc.) attached to the edge.
*
* <p>Unlike most other SWT layouts, this layout does not require layout data to be attached
* to each child control. Instead, member functions on the Layout are used to control
* the positioning of controls within the layout.</p>
*
* <p>The interface to this layout is intended to easily support drag-and-drop. Trim widgets
* can be added, removed, or inserted between other existing widgets and the layout will
* adjust accordingly. If one side of the layout contains no trim widgets, the central
* area will expand to reclaim the unused space.</p>
*
* <p>This layout must be told about every widget that it is supposed to arrange. If the
* composite contains additional widgets, they will not be moved by the layout and may
* be arranged through other means.</p>
*
* @since 3.0
*/
public class TrimLayout extends Layout implements ICachingLayout {
private static final TrimLayoutData defaultData = new TrimLayoutData();
private SizeCache centerArea = new SizeCache();
private List[] controls;
private int[] trimSizes;
private int marginWidth;
private int marginHeight;
private int topSpacing;
private int bottomSpacing;
private int leftSpacing;
private int rightSpacing;
private Map mapPartOntoTrimData = new HashMap();
// Position constants -- correspond to indices in the controls array, above.
private static final int TOP = 0;
private static final int BOTTOM = 1;
private static final int LEFT = 2;
private static final int RIGHT = 3;
private static final int NONTRIM = 4;
private class TrimData {
int controlsIndex;
SizeCache cache;
TrimData(int index, SizeCache cache) {
this.cache = cache;
this.controlsIndex = index;
}
}
/**
* Creates a new (initially empty) trim layout.
*/
public TrimLayout() {
controls = new List[4];
trimSizes = new int[controls.length];
for (int idx = 0; idx < controls.length; idx++) {
controls[idx] = new LinkedList();
trimSizes[idx] = SWT.DEFAULT;
}
}
/**
* Sets the empty space surrounding the center area. This whitespace is
* located between the trim and the central widget.
*/
public void setSpacing(int left, int right, int top, int bottom) {
leftSpacing = left;
rightSpacing = right;
topSpacing = top;
bottomSpacing = bottom;
}
/**
* Sets the empty space around the outside of the layout. This whitespace
* is located outside the trim widgets.
*
* @param marginWidth
* @param marginHeight
*/
public void setMargins(int marginWidth, int marginHeight) {
this.marginWidth = marginWidth;
this.marginHeight = marginHeight;
}
/**
* Converts an SWT position constant into an index in the controls array
*
* @param positionConstant one of SWT.LEFT, SWT.RIGHT, SWT.TOP, or SWT.BOTTOM
* @return an index into the controls array
*/
private int convertSwtConstantToIndex(int positionConstant) {
switch(positionConstant) {
case SWT.TOP: return TOP;
case SWT.BOTTOM: return BOTTOM;
case SWT.LEFT: return LEFT;
case SWT.RIGHT: return RIGHT;
}
return 0;
}
/**
* Converts an index into the controls array into the corresponding SWT constants.
*
* @param index
* @return
*/
private int convertIndexToSwtConstant(int index) {
switch(index) {
case TOP: return SWT.TOP;
case BOTTOM: return SWT.BOTTOM;
case LEFT: return SWT.LEFT;
case RIGHT: return SWT.RIGHT;
case NONTRIM: return SWT.DEFAULT;
}
return 0;
}
/**
* This method separates resizable controls from non-resizable controls.
*
* @param input the list of SizeCache to filter
* @param resizable will contain resizable controls from the input list
* @param nonResizable will contain non-resizable controls from the input list
* @param width if true, we're interested in horizontally-resizable controls. Else we're interested in
* vertically resizable controls
*/
private static void filterResizable(List input, List resizable, List nonResizable, boolean width) {
Iterator iter = input.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
if (isResizable(next.getControl(), width)) {
resizable.add(next);
} else {
nonResizable.add(next);
}
}
}
private static boolean isResizable(Control control, boolean horizontally) {
TrimLayoutData data = getData(control);
if (!data.resizable) {
return false;
}
if (horizontally) {
return data.widthHint == SWT.DEFAULT;
} else {
return data.heightHint == SWT.DEFAULT;
}
}
private static TrimLayoutData getData(Control control) {
TrimLayoutData data = (TrimLayoutData)control.getLayoutData();
if (data == null) {
data = defaultData;
}
return data;
}
private static Point computeSize(SizeCache toCompute, int widthHint, int heightHint) {
TrimLayoutData data = getData(toCompute.getControl());
if (widthHint == SWT.DEFAULT) {
widthHint = data.widthHint;
}
if (heightHint == SWT.DEFAULT) {
heightHint = data.heightHint;
}
if (widthHint == SWT.DEFAULT || heightHint == SWT.DEFAULT) {
return toCompute.computeSize(widthHint, heightHint);
}
return new Point(widthHint, heightHint);
}
private static int getSize(SizeCache toCompute, int hint, boolean width) {
if (width) {
return computeSize(toCompute, SWT.DEFAULT, hint).x;
} else {
return computeSize(toCompute, hint, SWT.DEFAULT).y;
}
}
/**
* Computes the maximum dimensions of controls in the given list
*
* @param controls a list of SizeCaches
* @return
*/
private static int maxDimension(List controls, int hint, boolean width) {
if (hint == SWT.DEFAULT) {
int result = 0;
Iterator iter = controls.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
result = Math.max(getSize(next, SWT.DEFAULT, width), result);
}
return result;
}
List resizable = new ArrayList(controls.size());
List nonResizable = new ArrayList(controls.size());
filterResizable(controls, resizable, nonResizable, width);
int result = 0;
int usedHeight = 0;
Iterator iter = nonResizable.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
Point nextSize = computeSize(next, SWT.DEFAULT, SWT.DEFAULT);
if (width) {
result = Math.max(result, nextSize.x);
usedHeight += nextSize.y;
} else {
result = Math.max(result, nextSize.y);
usedHeight += nextSize.x;
}
}
if (resizable.size() > 0) {
int individualHint = (hint - usedHeight) / resizable.size();
iter = resizable.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
result = Math.max(result, getSize(next, individualHint, width));
}
}
return result;
}
/**
* Sets the trimSize (pixels) for the given side of the layout. If SWT.DEFAULT, then
* the trim size will be computed from child controls.
*
* @param position one of SWT.TOP, SWT.BOTTOM, SWT.LEFT, SWT.RIGHT
* @param size
*/
public void setTrimSize(int position, int size) {
int idx = convertSwtConstantToIndex(position);
trimSizes[idx] = size;
}
/**
* Returns the location of the given trim control. For example, returns SWT.LEFT
* if the control is docked on the left, SWT.RIGHT if docked on the right, etc.
* Returns SWT.DEFAULT if the given control is not a trim control.
*
* @param trimControl control to query
* @return one of SWT.LEFT, SWT.RIGHT, SWT.TOP, SWT.BOTTOM, or SWT.DEFAULT
*/
public int getTrimLocation(Control trimControl) {
return convertIndexToSwtConstant(getIndex(trimControl));
}
/**
* Adds the given control to the layout's trim at the given location.
* This is equivalent to <code>addTrim(control, location, null)</code>.
*
* @param control new trim widget to be added
* @param location one of SWT.TOP, SWT.BOTTOM, SWT.LEFT, SWT.RIGHT
*/
public void addTrim(Control control, int location) {
addTrim(control, location, null);
}
/**
* Adds the given control to the layout's trim. Note that this must be called
* for every trim control. If the given widget is already a trim
* widget, it will be moved to the new position. Specifying a position allows
* a new widget to be inserted between existing trim widgets.
*
* <p>For example, this method allows the caller to say "insert this new control
* as trim along the bottom of the layout, to the left of this existing control".</p>
*
* @param control new trim widget to be added
* @param location one of SWT.TOP, SWT.BOTTOM, SWT.LEFT, SWT.RIGHT
* @param position if null, the control will be inserted as the last trim widget on
* this side of the layout. Otherwise, the control will be inserted before the given
* widget.
*/
public void addTrim(Control control, int location, Control position) {
removeTrim(control);
int index = convertSwtConstantToIndex(location);
List list = controls[index];
SizeCache cache = new SizeCache(control);
insertBefore(list, cache, position);
mapPartOntoTrimData.put(control, new TrimData(index, cache));
}
/**
* Inserts the given object into the list before the specified position.
* If the given position is null, the object is inserted at the end of the list.
*
* @param list a list of SizeCache
*/
private static void insertBefore(List list, SizeCache cache, Control position) {
if (position != null) {
int insertionPoint = 0;
Iterator iter = list.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
if (next.getControl() == position) {
break;
}
insertionPoint++;
}
list.add(insertionPoint, cache);
} else {
list.add(cache);
}
}
private static void remove(List list, Control toRemove) {
SizeCache target = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
if (next.getControl() == toRemove) {
target = next;
break;
}
}
list.remove(target);
}
/**
* Removes the given trim widget. Note that this has no effect if the
* widget is not a trim widget.
*
* @param toRemove
*/
public void removeTrim(Control toRemove) {
int idx = getIndex(toRemove);
// If this isn't a trim widget.
if (idx == NONTRIM) {
return;
}
remove(controls[idx], toRemove);
mapPartOntoTrimData.remove(toRemove);
}
/**
* Returns an index into the controls array, above, indicating the position
* where this trim control is located.
*
* @param toQuery
* @return
*/
private int getIndex(Control toQuery) {
TrimData data = (TrimData)mapPartOntoTrimData.get(toQuery);
if (data == null) {
return NONTRIM;
}
return data.controlsIndex;
}
/**
* Removes any disposed widgets from this layout
*/
private void removeDisposed() {
for (int idx = 0; idx < controls.length; idx++) {
List ctrl = controls[idx];
if (ctrl != null) {
Iterator iter = ctrl.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
Control nextControl = next.getControl();
if (nextControl.isDisposed() || getIndex(nextControl) != idx) {
iter.remove();
}
}
}
}
}
/**
* Returns the size of the trim on each side of the layout
*
* @return an array of trim sizes (pixels). See the index constants,
* above, for the meaning of the indices.
*/
private int[] getTrimSizes(int widthHint, int heightHint) {
int[] trimSize = new int[controls.length];
for (int idx = 0; idx < trimSizes.length; idx++) {
if (controls[idx].isEmpty()) {
trimSize[idx] = 0;
} else {
trimSize[idx] = trimSizes[idx];
}
}
if (trimSize[TOP] == SWT.DEFAULT) {
trimSize[TOP] = maxDimension(controls[TOP], widthHint, false);
}
if (trimSize[BOTTOM] == SWT.DEFAULT) {
trimSize[BOTTOM] = maxDimension(controls[BOTTOM], widthHint, false);
}
if (trimSize[LEFT] == SWT.DEFAULT) {
trimSize[LEFT] = maxDimension(controls[LEFT], heightHint, true);
}
if (trimSize[RIGHT] == SWT.DEFAULT) {
trimSize[RIGHT] = maxDimension(controls[RIGHT], heightHint, true);
}
return trimSize;
}
/* (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) {
Point result = new Point(wHint, hHint);
int[] trimSize = getTrimSizes(wHint, hHint);
int horizontalTrim = trimSize[LEFT] + trimSize[RIGHT] + (2 * marginWidth) + leftSpacing + rightSpacing;
int verticalTrim = trimSize[TOP] + trimSize[BOTTOM] + (2 * marginHeight) + topSpacing + bottomSpacing;
Point innerSize = centerArea.computeSize(
wHint == SWT.DEFAULT ? wHint : wHint - horizontalTrim,
hHint == SWT.DEFAULT ? hHint: hHint - verticalTrim);
if (wHint == SWT.DEFAULT) {
result.x = innerSize.x + horizontalTrim;
} else if (hHint == SWT.DEFAULT) {
result.y = innerSize.y + verticalTrim;
}
return new Point(0,0);
}
/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Layout#layout(org.eclipse.swt.widgets.Composite, boolean)
*/
protected void layout(Composite composite, boolean flushCache) {
removeDisposed();
Rectangle clientArea = composite.getClientArea();
clientArea.x += marginWidth;
clientArea.width -= 2 * marginWidth;
clientArea.y += marginHeight;
clientArea.height -= 2 * marginHeight;
int[] trimSize = getTrimSizes(clientArea.width, clientArea.height);
int leftOfLayout = clientArea.x;
int leftOfCenterPane = leftOfLayout + trimSize[LEFT] + leftSpacing;
int widthOfCenterPane = clientArea.width - trimSize[LEFT] - trimSize[RIGHT] - leftSpacing - rightSpacing;
int rightOfCenterPane = clientArea.x + clientArea.width - trimSize[RIGHT];
int topOfLayout = clientArea.y;
int topOfCenterPane = topOfLayout + trimSize[TOP] + topSpacing;
int heightOfCenterPane = clientArea.height - trimSize[TOP] - trimSize[BOTTOM] - topSpacing - bottomSpacing;
int bottomOfCenterPane = clientArea.y + clientArea.height - trimSize[BOTTOM];
arrange(new Rectangle(leftOfLayout, topOfLayout, clientArea.width, trimSize[TOP]), controls[TOP], true);
arrange(new Rectangle(leftOfCenterPane, bottomOfCenterPane, widthOfCenterPane, trimSize[BOTTOM]), controls[BOTTOM], true);
arrange(new Rectangle(leftOfLayout, topOfCenterPane, trimSize[LEFT], clientArea.height - trimSize[TOP]), controls[LEFT], false);
arrange(new Rectangle(rightOfCenterPane, topOfCenterPane, trimSize[RIGHT], clientArea.height - trimSize[TOP]), controls[RIGHT], false);
if (centerArea.getControl() != null) {
centerArea.getControl().setBounds(leftOfCenterPane, topOfCenterPane, widthOfCenterPane, heightOfCenterPane);
}
}
private void flushCaches() {
for (int idx = 0; idx < controls.length; idx++) {
List ctrl = controls[idx];
if (ctrl != null) {
Iterator iter = ctrl.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
next.flush();
}
}
}
centerArea.flush();
}
/**
* Arranges all the given controls in a horizontal row that fills the given rectangle.
*
* @param area area to be filled by the controls
* @param controls a list of SizeCaches for controls that will span the rectangle
*/
private static void arrange(Rectangle area, List controls, boolean horizontally) {
Point currentPosition = new Point(area.x, area.y);
List resizable = new ArrayList(controls.size());
List nonResizable = new ArrayList(controls.size());
filterResizable(controls, resizable, nonResizable, horizontally);
int[] sizes = new int[nonResizable.size()];
int idx = 0;
int used = 0;
int hint = Geometry.getDimension(area, !horizontally);
// Compute the sizes of non-resizable controls
Iterator iter = nonResizable.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
sizes[idx] = getSize(next, hint, horizontally);
used += sizes[idx];
idx++;
}
int available = Geometry.getDimension(area, horizontally) - used;
idx = 0;
int remainingResizable = resizable.size();
iter = controls.iterator();
while (iter.hasNext()) {
SizeCache next = (SizeCache)iter.next();
int thisSize;
if (isResizable(next.getControl(), horizontally)) {
thisSize = available / remainingResizable;
available -= thisSize;
remainingResizable--;
} else {
thisSize = sizes[idx];
idx++;
}
if (horizontally) {
next.getControl().setBounds(currentPosition.x, currentPosition.y, thisSize, hint);
currentPosition.x += thisSize;
} else {
next.getControl().setBounds(currentPosition.x, currentPosition.y, hint, thisSize);
currentPosition.y += thisSize;
}
}
}
/**
* Sets the widget that will occupy the central area of the layout. Typically,
* this will be a composite that contains the main widgetry of the application.
*
* @param composite control that will occupy the center of the layout, or null if none
*/
public void setCenterControl(Control center) {
centerArea.setControl(center);
}
/**
* Returns the control in the center of this layout
*
* @return
*/
public Control getCenterControl() {
return centerArea.getControl();
}
/* (non-Javadoc)
* @see org.eclipse.ui.internal.layout.ICachingLayout#flush(org.eclipse.swt.widgets.Control)
*/
public void flush(Control dirtyControl) {
TrimData data = (TrimData)mapPartOntoTrimData.get(dirtyControl);
if (data == null) {
if (dirtyControl == centerArea.getControl()) {
centerArea.flush();
}
} else {
data.cache.flush();
}
}
}