@qooxdoo/framework
Version:
The JS Framework for Coders
554 lines (442 loc) • 14.6 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* A vertical box layout.
*
* The vertical box layout lays out widgets in a vertical column, from top
* to bottom.
*
* *Features*
*
* * Minimum and maximum dimensions
* * Prioritized growing/shrinking (flex)
* * Margins (with vertical collapsing)
* * Auto sizing (ignoring percent values)
* * Percent heights (not relevant for size hint)
* * Alignment (child property {@link qx.ui.core.LayoutItem#alignY} is ignored)
* * Vertical spacing (collapsed with margins)
* * Reversed children layout (from last to first)
* * Horizontal children stretching (respecting size hints)
*
* *Item Properties*
*
* <ul>
* <li><strong>flex</strong> <em>(Integer)</em>: The flexibility of a layout item determines how the container
* distributes remaining empty space among its children. If items are made
* flexible, they can grow or shrink accordingly. Their relative flex values
* determine how the items are being resized, i.e. the larger the flex ratio
* of two items, the larger the resizing of the first item compared to the
* second.
*
* If there is only one flex item in a layout container, its actual flex
* value is not relevant. To disallow items to become flexible, set the
* flex value to zero.
* </li>
* <li><strong>height</strong> <em>(String)</em>: Allows to define a percent
* height for the item. The height in percent, if specified, is used instead
* of the height defined by the size hint. The minimum and maximum height still
* takes care of the element's limits. It has no influence on the layout's
* size hint. Percent values are mostly useful for widgets which are sized by
* the outer hierarchy.
* </li>
* </ul>
*
* *Example*
*
* Here is a little example of how to use the vertical box layout.
*
* <pre class="javascript">
* var layout = new qx.ui.layout.VBox();
* layout.setSpacing(4); // apply spacing
*
* var container = new qx.ui.container.Composite(layout);
*
* container.add(new qx.ui.core.Widget());
* container.add(new qx.ui.core.Widget());
* container.add(new qx.ui.core.Widget());
* </pre>
*
* *External Documentation*
*
* See <a href='http://manual.qooxdoo.org/${qxversion}/pages/layout/box.html'>extended documentation</a>
* and links to demos for this layout.
*
*/
qx.Class.define("qx.ui.layout.VBox",
{
extend : qx.ui.layout.Abstract,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param spacing {Integer?0} The spacing between child widgets {@link #spacing}.
* @param alignY {String?"top"} Vertical alignment of the whole children
* block {@link #alignY}.
* @param separator {String|qx.ui.decoration.IDecorator?} A separator to be rendered between the items
*/
construct : function(spacing, alignY, separator)
{
this.base(arguments);
if (spacing) {
this.setSpacing(spacing);
}
if (alignY) {
this.setAlignY(alignY);
}
if (separator) {
this.setSeparator(separator);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/**
* Vertical alignment of the whole children block. The vertical
* alignment of the child is completely ignored in VBoxes (
* {@link qx.ui.core.LayoutItem#alignY}).
*/
alignY :
{
check : [ "top", "middle", "bottom" ],
init : "top",
apply : "_applyLayoutChange"
},
/**
* Horizontal alignment of each child. Can be overridden through
* {@link qx.ui.core.LayoutItem#alignX}.
*/
alignX :
{
check : [ "left", "center", "right" ],
init : "left",
apply : "_applyLayoutChange"
},
/** Vertical spacing between two children */
spacing :
{
check : "Integer",
init : 0,
apply : "_applyLayoutChange"
},
/** Separator lines to use between the objects */
separator :
{
check : "Decorator",
nullable : true,
apply : "_applyLayoutChange"
},
/** Whether the actual children list should be laid out in reversed order. */
reversed :
{
check : "Boolean",
init : false,
apply : "_applyReversed"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__heights : null,
__flexs : null,
__enableFlex : null,
__children : null,
/*
---------------------------------------------------------------------------
HELPER METHODS
---------------------------------------------------------------------------
*/
// property apply
_applyReversed : function()
{
// easiest way is to invalidate the cache
this._invalidChildrenCache = true;
// call normal layout change
this._applyLayoutChange();
},
/**
* Rebuilds caches for flex and percent layout properties
*/
__rebuildCache : function()
{
var children = this._getLayoutChildren();
var length = children.length;
var enableFlex = false;
var reuse = this.__heights && this.__heights.length != length && this.__flexs && this.__heights;
var props;
// Sparse array (keep old one if lengths has not been modified)
var heights = reuse ? this.__heights : new Array(length);
var flexs = reuse ? this.__flexs : new Array(length);
// Reverse support
if (this.getReversed()) {
children = children.concat().reverse();
}
// Loop through children to preparse values
for (var i=0; i<length; i++)
{
props = children[i].getLayoutProperties();
if (props.height != null) {
heights[i] = parseFloat(props.height) / 100;
}
if (props.flex != null)
{
flexs[i] = props.flex;
enableFlex = true;
} else {
// reset (in case the index of the children changed: BUG #3131)
flexs[i] = 0;
}
}
// Store data
if (!reuse)
{
this.__heights = heights;
this.__flexs = flexs;
}
this.__enableFlex = enableFlex;
this.__children = children;
// Clear invalidation marker
delete this._invalidChildrenCache;
},
/*
---------------------------------------------------------------------------
LAYOUT INTERFACE
---------------------------------------------------------------------------
*/
// overridden
verifyLayoutProperty : qx.core.Environment.select("qx.debug",
{
"true" : function(item, name, value)
{
this.assert(name === "flex" || name === "height", "The property '"+name+"' is not supported by the VBox layout!");
if (name =="height")
{
this.assertMatch(value, qx.ui.layout.Util.PERCENT_VALUE);
}
else
{
// flex
this.assertNumber(value);
this.assert(value >= 0);
}
},
"false" : null
}),
// overridden
renderLayout : function(availWidth, availHeight, padding)
{
// Rebuild flex/height caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
// Cache children
var children = this.__children;
var length = children.length;
var util = qx.ui.layout.Util;
// Compute gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
if (separator) {
var gaps = util.computeVerticalSeparatorGaps(children, spacing, separator);
} else {
var gaps = util.computeVerticalGaps(children, spacing, true);
}
// First run to cache children data and compute allocated height
var i, child, height, percent;
var heights = [], hint;
var allocatedHeight = gaps;
for (i=0; i<length; i+=1)
{
percent = this.__heights[i];
hint = children[i].getSizeHint();
height = percent != null ?
Math.floor((availHeight - gaps) * percent) :
hint.height;
// Limit computed value
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
heights.push(height);
allocatedHeight += height;
}
// Flex support (growing/shrinking)
if (this.__enableFlex && allocatedHeight != availHeight)
{
var flexibles = {};
var flex, offset;
for (i=0; i<length; i+=1)
{
flex = this.__flexs[i];
if (flex > 0)
{
hint = children[i].getSizeHint();
flexibles[i]=
{
min : hint.minHeight,
value : heights[i],
max : hint.maxHeight,
flex : flex
};
}
}
var result = util.computeFlexOffsets(flexibles, availHeight, allocatedHeight);
for (i in result)
{
offset = result[i].offset;
heights[i] += offset;
allocatedHeight += offset;
}
}
// Start with top coordinate
var top = children[0].getMarginTop();
// Alignment support
if (allocatedHeight < availHeight && this.getAlignY() != "top")
{
top = availHeight - allocatedHeight;
if (this.getAlignY() === "middle") {
top = Math.round(top / 2);
}
}
// Layouting children
var hint, left, width, height, marginBottom, marginLeft, marginRight;
// Pre configure separators
this._clearSeparators();
// Compute separator height
if (separator)
{
var separatorInsets = qx.theme.manager.Decoration.getInstance().resolve(separator).getInsets();
var separatorHeight = separatorInsets.top + separatorInsets.bottom;
}
// Render children and separators
for (i=0; i<length; i+=1)
{
child = children[i];
height = heights[i];
hint = child.getSizeHint();
marginLeft = child.getMarginLeft();
marginRight = child.getMarginRight();
// Find usable width
width = Math.max(hint.minWidth, Math.min(availWidth-marginLeft-marginRight, hint.maxWidth));
// Respect horizontal alignment
left = util.computeHorizontalAlignOffset(child.getAlignX()||this.getAlignX(), width, availWidth, marginLeft, marginRight);
// Add collapsed margin
if (i > 0)
{
// Whether a separator has been configured
if (separator)
{
// add margin of last child and spacing
top += marginBottom + spacing;
// then render the separator at this position
this._renderSeparator(separator, {
top : top + padding.top,
left : padding.left,
height : separatorHeight,
width : availWidth
});
// and finally add the size of the separator, the spacing (again) and the top margin
top += separatorHeight + spacing + child.getMarginTop();
}
else
{
// Support margin collapsing when no separator is defined
top += util.collapseMargins(spacing, marginBottom, child.getMarginTop());
}
}
// Layout child
child.renderLayout(left + padding.left, top + padding.top, width, height);
// Add height
top += height;
// Remember bottom margin (for collapsing)
marginBottom = child.getMarginBottom();
}
},
// overridden
_computeSizeHint : function()
{
// Rebuild flex/height caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var util = qx.ui.layout.Util;
var children = this.__children;
// Initialize
var minHeight=0, height=0, percentMinHeight=0;
var minWidth=0, width=0;
var child, hint, margin;
// Iterate over children
for (var i=0, l=children.length; i<l; i+=1)
{
child = children[i];
hint = child.getSizeHint();
// Sum up heights
height += hint.height;
// Detect if child is shrinkable or has percent height and update minHeight
var flex = this.__flexs[i];
var percent = this.__heights[i];
if (flex) {
minHeight += hint.minHeight;
} else if (percent) {
percentMinHeight = Math.max(percentMinHeight, Math.round(hint.minHeight/percent));
} else {
minHeight += hint.height;
}
// Build horizontal margin sum
margin = child.getMarginLeft() + child.getMarginRight();
// Find biggest width
if ((hint.width+margin) > width) {
width = hint.width + margin;
}
// Find biggest minWidth
if ((hint.minWidth+margin) > minWidth) {
minWidth = hint.minWidth + margin;
}
}
minHeight += percentMinHeight;
// Respect gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
if (separator) {
var gaps = util.computeVerticalSeparatorGaps(children, spacing, separator);
} else {
var gaps = util.computeVerticalGaps(children, spacing, true);
}
// Return hint
return {
minHeight : minHeight + gaps,
height : height + gaps,
minWidth : minWidth,
width : width
};
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function() {
this.__heights = this.__flexs = this.__children = null;
}
});