/******************************************************************************
* Copyright (c) 2016 Oracle
* 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:
* Ling Hao - initial implementation and ongoing maintenance
* Shenxue Zhou - initial implementation and ongoing maintenance
******************************************************************************/
package org.eclipse.sapphire.ui.swt.gef.layout;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.draw2d.AbstractHintLayout;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.geometry.Transposer;
import org.eclipse.sapphire.ui.diagram.shape.def.SequenceLayoutDef;
import org.eclipse.sapphire.ui.diagram.shape.def.SequenceLayoutOrientation;
import org.eclipse.sapphire.ui.swt.gef.figures.TextFigure;
import org.eclipse.swt.SWT;
/**
* Arranges figures in a single row or column. Orientation can be set to produce
* either a row or column layout. This layout tries to fit all children within
* the parent's client area. To do this, it compresses the children by some
* amount, but will not compress them smaller than their minimum size. If a
* child's preferred size is smaller than the row's or column's minor dimension,
* the layout can be configured to stretch the child.
*
* @author <a href="mailto:ling.hao@oracle.com">Ling Hao</a>
* @author <a href="mailto:shenxue.zhou@oracle.com">Shenxue Zhou</a>
*
*/
public class SapphireSequenceLayout extends AbstractHintLayout {
/** The layout contraints */
private Map<IFigure,Object> constraints = new HashMap<IFigure,Object>();
/**
* Transposer object that may be used in layout calculations. Will be
* automatically enabled/disabled dependent on the default and the actual
* orientation.
*
* @noreference This field is not intended to be referenced by clients.
*/
private Transposer transposer = new Transposer();
/**
* The horizontal property.
*
*/
private boolean horizontal;
/**
* Space in pixels between Figures
*
*/
private int spacing;
/**
* Margin insets
*
*/
private Insets marginInsets;
/**
* Constructs a SapphireSequenceLayout with a specified SequenceLayoutDef
*/
public SapphireSequenceLayout(SequenceLayoutDef def) {
setHorizontal(def.getOrientation().content() == SequenceLayoutOrientation.HORIZONTAL);
setSpacing(def.getSpacing().content());
this.marginInsets = LayoutUtil.calculateMargin(def);
}
public void setHorizontal(boolean flag) {
if (horizontal == flag)
return;
invalidate();
horizontal = flag;
updateTransposerEnabledState();
}
public boolean isHorizontal() {
return horizontal;
}
private Dimension calculateChildrenSize(List<IFigure> children, int wHint, int hHint, boolean preferred) {
Dimension childSize;
IFigure child;
int height = 0, width = 0;
for (int i = 0; i < children.size(); i++) {
child = children.get(i);
childSize = transposer.t(preferred ? getChildPreferredSize(child,
wHint, hHint) : getChildMinimumSize(child, wHint, hHint));
Insets inset = new Insets();
SapphireSequenceLayoutConstraint constraint = (SapphireSequenceLayoutConstraint)getConstraint(child);
if (constraint != null) {
inset = transposer.t(constraint.getMarginInset());
}
height += childSize.height + inset.top + inset.bottom;
width = Math.max(width, childSize.width + inset.left + inset.right);
}
return new Dimension(width, height);
}
private Dimension calculateChildrenMaximumSize(List<IFigure> children) {
Dimension childSize;
IFigure child;
int height = 0, width = Integer.MAX_VALUE;
for (int i = 0; i < children.size(); i++) {
child = children.get(i);
childSize = transposer.t(getChildCellMaximumSize(child));
Insets inset = new Insets();
SapphireSequenceLayoutConstraint constraint = (SapphireSequenceLayoutConstraint)getConstraint(child);
if (constraint != null) {
inset = transposer.t(constraint.getMarginInset());
}
width = Math.min(width, childSize.width + (childSize.width < Integer.MAX_VALUE ? inset.left + inset.right : 0));
if (childSize.height < Integer.MAX_VALUE ) {
if (height < Integer.MAX_VALUE) {
height += childSize.height + inset.top + inset.bottom;
}
}
else {
height = Integer.MAX_VALUE;
}
}
return new Dimension(width, height);
}
/**
* Calculates the minimum size of the container based on the given hints. If
* this is a vertically-oriented sequence Layout, then only the widthHint is
* respected (which means that the children can be as tall as they desire).
* In this case, the minimum width is that of the widest child, and the
* minimum height is the sum of the minimum heights of all children, plus
* the spacing between them. The border and insets of the container figure
* are also accounted for.
*
* @param container
* the figure whose minimum size has to be calculated
* @param wHint
* the width hint (the desired width of the container)
* @param hHint
* the height hint (the desired height of the container)
* @return the minimum size of the container
* @see #getMinimumSize(IFigure, int, int)
* @since 2.1
*/
@SuppressWarnings( "unchecked" )
protected Dimension calculateMinimumSize(IFigure container, int wHint, int hHint) {
Insets insets = container.getInsets();
if (isHorizontal()) {
wHint = -1;
if (hHint >= 0)
hHint = Math.max(0, hHint - insets.getHeight());
} else {
hHint = -1;
if (wHint >= 0)
wHint = Math.max(0, wHint - insets.getWidth());
}
List<IFigure> children = container.getChildren();
Dimension minSize = calculateChildrenSize(children, wHint, hHint, false);
// Do a second pass, if necessary
if (wHint >= 0 && minSize.width > wHint) {
minSize = calculateChildrenSize(children, minSize.width, hHint, false);
} else if (hHint >= 0 && minSize.width > hHint) {
minSize = calculateChildrenSize(children, wHint, minSize.width, false);
}
minSize.height += Math.max(0, children.size() - 1) * spacing;
Insets inset = transposer.t(this.marginInsets);
minSize.width += inset.left + inset.right;
minSize.height += inset.top + inset.bottom;
return transposer.t(minSize)
.expand(insets.getWidth(), insets.getHeight())
.union(getBorderPreferredSize(container));
}
/**
* Calculates the preferred size of the container based on the given hints.
* If this is a vertically-oriented sequence Layout, then only the widthHint
* is respected (which means that the children can be as tall as they
* desire). In this case, the preferred width is that of the widest child,
* and the preferred height is the sum of the preferred heights of all
* children, plus the spacing between them. The border and insets of the
* container figure are also accounted for.
*
* @param container
* the figure whose preferred size has to be calculated
* @param wHint
* the width hint (the desired width of the container)
* @param hHint
* the height hint (the desired height of the container)
* @return the preferred size of the container
* @see #getPreferredSize(IFigure, int, int)
* @since 2.0
*/
@SuppressWarnings( "unchecked" )
protected Dimension calculatePreferredSize(IFigure container, int wHint,
int hHint) {
Insets insets = container.getInsets();
if (isHorizontal()) {
wHint = -1;
if (hHint >= 0)
hHint = Math.max(0, hHint - insets.getHeight());
} else {
hHint = -1;
if (wHint >= 0)
wHint = Math.max(0, wHint - insets.getWidth());
}
List<IFigure> children = container.getChildren();
Dimension prefSize = calculateChildrenSize(children, wHint, hHint, true);
// Do a second pass, if necessary
if (wHint >= 0 && prefSize.width > wHint) {
prefSize = calculateChildrenSize(children, prefSize.width, hHint, true);
} else if (hHint >= 0 && prefSize.width > hHint) {
prefSize = calculateChildrenSize(children, wHint, prefSize.width, true);
}
// Constrain the preferred width by container's max width and min width
Dimension maxSize = calculateChildrenMaximumSize(children);
Dimension minSize = calculateChildrenSize(children, wHint, hHint, false);
prefSize.width = Math.min(maxSize.width, prefSize.width);
prefSize.width = Math.max(minSize.width, prefSize.width);
prefSize.height += Math.max(0, children.size() - 1) * spacing;
Insets inset = transposer.t(this.marginInsets);
prefSize.width += inset.left + inset.right;
prefSize.height += inset.top + inset.bottom;
return transposer.t(prefSize)
.expand(insets.getWidth(), insets.getHeight())
.union(getBorderPreferredSize(container));
}
@SuppressWarnings( "unchecked" )
public Dimension calculateMaximumSize(IFigure container) {
List<IFigure> children = container.getChildren();
Dimension maxSize = calculateChildrenMaximumSize(children);
Insets marginInsets = transposer.t(this.marginInsets);
if (maxSize.width < Integer.MAX_VALUE) {
maxSize.width += marginInsets.left + marginInsets.right;
}
if (maxSize.height < Integer.MAX_VALUE) {
maxSize.height += Math.max(0, children.size() - 1) * spacing;
maxSize.height += marginInsets.top + marginInsets.bottom;
}
maxSize = transposer.t(maxSize);
Insets containerInsets = container.getInsets();
Dimension borderSize = getBorderPreferredSize(container);
if (maxSize.width < Integer.MAX_VALUE) {
maxSize.width += containerInsets.getWidth();
maxSize.width = Math.max(borderSize.width, maxSize.width);
}
if (maxSize.height < Integer.MAX_VALUE) {
maxSize.height += containerInsets.getHeight();
maxSize.height = Math.max(borderSize.height, maxSize.height);
}
return maxSize;
}
/**
* @param child
* the figure whose minimum size is to be determined
* @param wHint
* the width hint
* @param hHint
* the height hint
* @return the given figure's minimum size
* @since 3.3
*/
protected Dimension getChildMinimumSize(IFigure child, int wHint, int hHint) {
Dimension minSize = child.getMinimumSize(wHint, hHint).getCopy();
SapphireSequenceLayoutConstraint constraint = (SapphireSequenceLayoutConstraint)getConstraint(child);
if (constraint.minWidth > minSize.width)
{
minSize.width = constraint.minWidth;
}
if (constraint.minHeight > minSize.height)
{
minSize.height = constraint.minHeight;
}
return minSize;
}
/**
* @param child
* the figure whose preferred size is to be determined
* @param wHint
* the width hint
* @param hHint
* the height hint
* @return given figure's preferred size
* @since 3.3
*/
protected Dimension getChildPreferredSize(IFigure child, int wHint, int hHint) {
Dimension dimension = child.getPreferredSize(wHint, hHint).getCopy();
Dimension minSize = child.getMinimumSize(wHint, hHint);
SapphireSequenceLayoutConstraint constraint = (SapphireSequenceLayoutConstraint)getConstraint(child);
if (constraint.widthHint > minSize.width ) {
dimension.width = constraint.widthHint;
}
if (constraint.heightHint > minSize.height) {
dimension.height = constraint.heightHint;
}
if (constraint.minWidth > SWT.DEFAULT && dimension.width < constraint.minWidth) {
dimension.width = constraint.minWidth;
}
if (constraint.minHeight > SWT.DEFAULT && dimension.height < constraint.minHeight) {
dimension.height = constraint.minHeight;
}
if (constraint.maxWidth > SWT.DEFAULT && dimension.width > constraint.maxWidth) {
dimension.width = constraint.maxWidth;
}
if (constraint.maxHeight > SWT.DEFAULT && dimension.height > constraint.maxHeight) {
dimension.height = constraint.maxHeight;
}
return dimension;
}
protected Dimension getChildCellMaximumSize(IFigure child) {
Dimension dimension = child.getMaximumSize().getCopy();
SapphireSequenceLayoutConstraint constraint = (SapphireSequenceLayoutConstraint)getConstraint(child);
if (constraint.expandHorizontally) {
dimension.width = Integer.MAX_VALUE;
}
if (constraint.expandVertically) {
dimension.height = Integer.MAX_VALUE;
}
if (constraint.maxWidth > SWT.DEFAULT && constraint.maxWidth < Integer.MAX_VALUE ) {
dimension.width = Math.min(constraint.maxWidth, dimension.width);
}
if (constraint.maxHeight > SWT.DEFAULT && constraint.maxHeight < Integer.MAX_VALUE ) {
dimension.height = Math.min(constraint.maxHeight, dimension.height);
}
return dimension;
}
/**
* Returns {@link PositionConstants#VERTICAL} by default.
*
* @see org.eclipse.draw2d.OrderedLayout#getDefaultOrientation()
*/
protected int getDefaultOrientation() {
return PositionConstants.VERTICAL;
}
/**
* @return the spacing between children
*/
public int getSpacing() {
return spacing;
}
/**
* @see org.eclipse.draw2d.AbstractHintLayout#isSensitiveHorizontally(IFigure)
*/
protected boolean isSensitiveHorizontally(IFigure parent) {
return !isHorizontal();
}
/**
* @see org.eclipse.draw2d.AbstractHintLayout#isSensitiveVertically(IFigure)
*/
protected boolean isSensitiveVertically(IFigure parent) {
return isHorizontal();
}
/**
* @see org.eclipse.draw2d.LayoutManager#layout(IFigure)
*/
@SuppressWarnings( "unchecked" )
public void layout(IFigure parent) {
List<IFigure> children = parent.getChildren();
int numChildren = children.size();
Rectangle clientArea = transposer.t(parent.getClientArea());
Insets margins = transposer.t(this.marginInsets);
clientArea.x += margins.left;
clientArea.width -= (margins.left + margins.right);
int x = clientArea.x;
int y = clientArea.y;
int availableHeight = clientArea.height;
Dimension prefSizes[] = new Dimension[numChildren];
Dimension minSizes[] = new Dimension[numChildren];
Dimension maxCellSizes[] = new Dimension[numChildren];
Dimension maxChildShapeSizes[] = new Dimension[numChildren];
int extraHeights[] = new int[numChildren];
SapphireSequenceLayoutConstraint constraints[] = new SapphireSequenceLayoutConstraint[numChildren];
Insets marginInsets[] = new Insets[numChildren];
// Calculate the width and height hints. If it's a vertical sequence layout,
// then ignore the height hint (set it to -1); otherwise, ignore the
// width hint. These hints will be passed to the children of the parent
// figure when getting their preferred size.
int wHint = -1;
int hHint = -1;
if (isHorizontal()) {
hHint = parent.getClientArea(Rectangle.SINGLETON).height - (margins.top + margins.bottom);
} else {
wHint = parent.getClientArea(Rectangle.SINGLETON).width - (margins.left + margins.right);
}
/*
* Calculate sum of preferred heights of all children(totalHeight).
* Cache Preferred Sizes and Minimum Sizes of all children.
*/
IFigure child;
int totalHeight = 0;
int totalMinHeight = 0;
int prefMinSumHeight = 0;
int totalMargin = 0;
int expandCount = 0;
for (int i = 0; i < numChildren; i++) {
child = children.get(i);
SapphireSequenceLayoutConstraint constraint = (SapphireSequenceLayoutConstraint)getConstraint(child);
if (constraint == null)
setConstraint(child, constraint = new SapphireSequenceLayoutConstraint());
constraints[i] = constraint;
prefSizes[i] = transposer.t(getChildPreferredSize(child, wHint, hHint));
minSizes[i] = transposer.t(getChildMinimumSize(child, wHint, hHint));
maxCellSizes[i] = transposer.t(getChildCellMaximumSize(child));
maxChildShapeSizes[i] = transposer.t(child.getMaximumSize());
marginInsets[i] = transposer.t(constraint.getMarginInset());
totalHeight += prefSizes[i].height;
totalMinHeight += minSizes[i].height;
totalMargin += marginInsets[i].top + marginInsets[i].bottom;
// We need to expand the cell if the its constraint has "expand" bit on or
// one of the children has "expand" bit on
if (getMajorExpand(constraint) || maxChildShapeSizes[i].height > prefSizes[i].height) {
expandCount++;
}
}
totalHeight += (numChildren - 1) * spacing;
totalHeight += totalMargin + margins.top + margins.bottom;
totalMinHeight += (numChildren - 1) * spacing;
totalMinHeight += totalMargin + margins.top + margins.bottom;
prefMinSumHeight = totalHeight - totalMinHeight;
int amntShrinkHeight = totalHeight
- Math.max(availableHeight, totalMinHeight);
int extraHeight = -amntShrinkHeight;
if (amntShrinkHeight < 0) {
amntShrinkHeight = 0;
}
if (extraHeight <= 0) {
extraHeight = 0;
} else if (expandCount > 0) {
int averageExtraHeight = extraHeight / expandCount;
int limitedExpansionCount = 0;
int limitedExpansionHeightTotal = 0;
if (expandCount > 1) {
for (int i = 0; i < numChildren; i++) {
int prefHeight = prefSizes[i].height;
int maxCellHeight = maxCellSizes[i].height;
child = children.get(i);
SapphireSequenceLayoutConstraint constraint = constraints[i];
if (getMajorExpand(constraint) || maxCellHeight > prefHeight) {
// only limited expansion since the child figure has max size constraint.
if (maxCellHeight - prefHeight < averageExtraHeight) {
limitedExpansionCount++;
limitedExpansionHeightTotal += maxCellHeight - prefHeight;
}
}
}
}
int unlimitedExpansionAverage = limitedExpansionCount < expandCount ?
(extraHeight - limitedExpansionHeightTotal) / (expandCount - limitedExpansionCount) : 0;
for (int i = 0; i < numChildren; i++) {
int prefHeight = prefSizes[i].height;
int maxCellHeight = maxCellSizes[i].height;
child = children.get(i);
SapphireSequenceLayoutConstraint constraint = constraints[i];
if (getMajorExpand(constraint) || maxCellHeight > prefHeight) {
// only limited expansion
if (expandCount > 1 && (maxCellHeight - prefHeight < averageExtraHeight)) {
extraHeights[i] = maxCellHeight - prefHeight;
}
else {
extraHeights[i] = unlimitedExpansionAverage;
}
}
else {
extraHeights[i] = 0;
}
}
}
y += margins.top;
for (int i = 0; i < numChildren; i++) {
int amntShrinkCurrentHeight = 0;
child = children.get(i);
int prefHeight = prefSizes[i].height;
int minHeight = minSizes[i].height;
int prefWidth = prefSizes[i].width;
int minWidth = minSizes[i].width;
Insets marginInset = marginInsets[i];
Rectangle newBounds, availableBounds;
int availableBoundHeight;
SapphireSequenceLayoutConstraint constraint = constraints[i];
if (prefMinSumHeight != 0)
amntShrinkCurrentHeight = (prefHeight - minHeight)
* amntShrinkHeight / (prefMinSumHeight);
int height = prefHeight;
if (amntShrinkCurrentHeight != 0) {
height -= amntShrinkCurrentHeight;
newBounds = new Rectangle(x, y + marginInset.top, prefWidth, height);
amntShrinkHeight -= amntShrinkCurrentHeight;
prefMinSumHeight -= (prefHeight - minHeight);
}
else if (getMajorExpand(constraint) || maxChildShapeSizes[i].height > prefSizes[i].height ) {
height += extraHeights[i];
// If the expansion comes from child shape, let the child shape take up the extra space.
// Otherwise, the virtual cell takes up the extra space and we use its alignment to place
// the child shape
if (maxChildShapeSizes[i].height > prefSizes[i].height) {
newBounds = new Rectangle(x, y + marginInset.top, prefWidth, height);
} else {
int offset = 0;
// alignment
switch (getMajorAlignment(constraint)) {
case SWT.CENTER:
offset = extraHeights[i] >> 1;
break;
case SWT.RIGHT:
case SWT.BOTTOM:
offset = extraHeights[i];
break;
default:
break;
}
newBounds = new Rectangle(x, y + marginInset.top + offset, prefWidth, prefHeight);
}
} else {
newBounds = new Rectangle(x, y + marginInset.top, prefWidth, height);
}
availableBoundHeight = height;
int width = Math.min(prefWidth, maxCellSizes[i].width);
// if (getMinorExpand(constraint) && (child instanceof RectangleFigure))
// width = maxSizes[i].width;
width = Math.max(minWidth, Math.min(clientArea.width, width));
newBounds.width = width;
// // Shenxue: include the margins in the available area. Otherwise the direct edit box could be
// // too narrow for text that doesn't expand. It doesn't have any extra space around the text in
// // direct edit mode even when there are margins around the text.
// availableBounds = new Rectangle(x, y, clientArea.width,
// availableBoundHeight + marginInset.top + marginInset.bottom);
availableBounds = new Rectangle(x + marginInset.left, y + marginInset.top,
clientArea.width - marginInset.left - marginInset.right, availableBoundHeight);
if (maxChildShapeSizes[i].width > prefSizes[i].width)
{
newBounds.x += marginInset.left;
newBounds.width = clientArea.width - marginInset.left - marginInset.right;
}
else
{
// Honor alignment if there is extra space. It's consistent with how the direct cell
// editor locator places direct editor cell when direct editing text.
// Shenxue - based on discussion with Ling 7/10/2013
int adjust = clientArea.width - width - marginInset.left - marginInset.right;
if (adjust <= 0)
{
adjust = 0;
}
else
{
switch (getMinorAlignment(constraint))
{
case SWT.TOP:
case SWT.LEFT:
adjust = 0;
break;
case SWT.CENTER:
adjust = (adjust + 1) >> 1;
break;
default:
break;
}
}
newBounds.x += adjust + marginInset.left;
}
child.setBounds(transposer.t(newBounds));
if (child instanceof TextFigure) {
((TextFigure) child).setAvailableArea(transposer.t(availableBounds));
((TextFigure) child).setHorizontalAlignment(constraint.horizontalAlignment);
}
y += availableBoundHeight + spacing + marginInset.top + marginInset.bottom;
}
}
private int getMajorAlignment(SapphireSequenceLayoutConstraint constraint) {
return isHorizontal() ? constraint.horizontalAlignment : constraint.verticalAlignment;
}
private int getMinorAlignment(SapphireSequenceLayoutConstraint constraint) {
return isHorizontal() ? constraint.verticalAlignment : constraint.horizontalAlignment;
}
private boolean getMajorExpand(SapphireSequenceLayoutConstraint constraint) {
return isHorizontal() ? constraint.expandHorizontally : constraint.expandVertically;
}
/**
* Sets the amount of space between children.
*
* @param space
* the amount of space between children
* @since 2.0
*/
public void setSpacing(int space) {
spacing = space;
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.draw2d.LayoutManager#getConstraint(org.eclipse.draw2d.IFigure
* )
*/
public Object getConstraint(IFigure child) {
return constraints.get(child);
}
/**
* Sets the layout constraint of the given figure. The constraints can only
* be of type {@link org.eclipse.draw2d.GridData}.
*/
public void setConstraint(IFigure figure, Object newConstraint) {
super.setConstraint(figure, newConstraint);
if (newConstraint != null) {
constraints.put(figure, newConstraint);
}
}
/**
* Updates the enabled state of the {@link #transposer} in case the layout
* has a different orientation that its default one.
*/
private void updateTransposerEnabledState() {
// enable transposer if the current orientation differs from the default
// orientation, disable it otherwise
transposer.setEnabled((isHorizontal()
&& getDefaultOrientation() == PositionConstants.VERTICAL)
|| (!isHorizontal()
&& getDefaultOrientation() == PositionConstants.HORIZONTAL));
}
}