@qooxdoo/framework
Version:
The JS Framework for Coders
554 lines (443 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 horizontal box layout.
*
* The horizontal box layout lays out widgets in a horizontal row, from left
* to right.
*
* *Features*
*
* * Minimum and maximum dimensions
* * Prioritized growing/shrinking (flex)
* * Margins (with horizontal collapsing)
* * Auto sizing (ignoring percent values)
* * Percent widths (not relevant for size hint)
* * Alignment (child property {@link qx.ui.core.LayoutItem#alignX} is ignored)
* * Horizontal spacing (collapsed with margins)
* * Reversed children layout (from last to first)
* * Vertical 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>width</strong> <em>(String)</em>: Allows to define a percent
* width for the item. The width in percent, if specified, is used instead
* of the width defined by the size hint. The minimum and maximum width 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 HBox layout.
*
* <pre class="javascript">
* var layout = new qx.ui.layout.HBox();
* 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.HBox",
{
extend : qx.ui.layout.Abstract,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param spacing {Integer?0} The spacing between child widgets {@link #spacing}.
* @param alignX {String?"left"} Horizontal alignment of the whole children
* block {@link #alignX}.
* @param separator {String|qx.ui.decoration.IDecorator?} A separator to render between the items
*/
construct : function(spacing, alignX, separator)
{
this.base(arguments);
if (spacing) {
this.setSpacing(spacing);
}
if (alignX) {
this.setAlignX(alignX);
}
if (separator) {
this.setSeparator(separator);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/**
* Horizontal alignment of the whole children block. The horizontal
* alignment of the child is completely ignored in HBoxes (
* {@link qx.ui.core.LayoutItem#alignX}).
*/
alignX :
{
check : [ "left", "center", "right" ],
init : "left",
apply : "_applyLayoutChange"
},
/**
* Vertical alignment of each child. Can be overridden through
* {@link qx.ui.core.LayoutItem#alignY}.
*/
alignY :
{
check : [ "top", "middle", "bottom" ],
init : "top",
apply : "_applyLayoutChange"
},
/** Horizontal 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 :
{
__widths : 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.__widths && this.__widths.length != length && this.__flexs && this.__widths;
var props;
// Sparse array (keep old one if lengths has not been modified)
var widths = reuse ? this.__widths : 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.width != null) {
widths[i] = parseFloat(props.width) / 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.__widths = widths;
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 === "width", "The property '"+name+"' is not supported by the HBox layout!");
if (name =="width")
{
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/width 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.computeHorizontalSeparatorGaps(children, spacing, separator);
} else {
var gaps = util.computeHorizontalGaps(children, spacing, true);
}
// First run to cache children data and compute allocated width
var i, child, width, percent;
var widths = [], hint;
var allocatedWidth = gaps;
for (i=0; i<length; i+=1)
{
percent = this.__widths[i];
hint = children[i].getSizeHint();
width = percent != null ?
Math.floor((availWidth - gaps) * percent) :
hint.width;
// Limit computed value
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
widths.push(width);
allocatedWidth += width;
}
// Flex support (growing/shrinking)
if (this.__enableFlex && allocatedWidth != availWidth)
{
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.minWidth,
value : widths[i],
max : hint.maxWidth,
flex : flex
};
}
}
var result = util.computeFlexOffsets(flexibles, availWidth, allocatedWidth);
for (i in result)
{
offset = result[i].offset;
widths[i] += offset;
allocatedWidth += offset;
}
}
// Start with left coordinate
var left = children[0].getMarginLeft();
// Alignment support
if (allocatedWidth < availWidth && this.getAlignX() != "left")
{
left = availWidth - allocatedWidth;
if (this.getAlignX() === "center") {
left = Math.round(left / 2);
}
}
// Layouting children
var hint, top, height, width, marginRight, marginTop, marginBottom;
var spacing = this.getSpacing();
// Pre configure separators
this._clearSeparators();
// Compute separator width
if (separator)
{
var separatorInsets = qx.theme.manager.Decoration.getInstance().resolve(separator).getInsets();
var separatorWidth = separatorInsets.left + separatorInsets.right;
}
// Render children and separators
for (i=0; i<length; i+=1)
{
child = children[i];
width = widths[i];
hint = child.getSizeHint();
marginTop = child.getMarginTop();
marginBottom = child.getMarginBottom();
// Find usable height
height = Math.max(hint.minHeight, Math.min(availHeight-marginTop-marginBottom, hint.maxHeight));
// Respect vertical alignment
top = util.computeVerticalAlignOffset(child.getAlignY()||this.getAlignY(), height, availHeight, marginTop, marginBottom);
// Add collapsed margin
if (i > 0)
{
// Whether a separator has been configured
if (separator)
{
// add margin of last child and spacing
left += marginRight + spacing;
// then render the separator at this position
this._renderSeparator(separator, {
left : left + padding.left,
top : padding.top,
width : separatorWidth,
height : availHeight
});
// and finally add the size of the separator, the spacing (again) and the left margin
left += separatorWidth + spacing + child.getMarginLeft();
}
else
{
// Support margin collapsing when no separator is defined
left += util.collapseMargins(spacing, marginRight, child.getMarginLeft());
}
}
// Layout child
child.renderLayout(left + padding.left, top + padding.top, width, height);
// Add width
left += width;
// Remember right margin (for collapsing)
marginRight = child.getMarginRight();
}
},
// overridden
_computeSizeHint : function()
{
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var util = qx.ui.layout.Util;
var children = this.__children;
// Initialize
var minWidth=0, width=0, percentMinWidth=0;
var minHeight=0, height=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 widths
width += hint.width;
// Detect if child is shrinkable or has percent width and update minWidth
var flex = this.__flexs[i];
var percent = this.__widths[i];
if (flex) {
minWidth += hint.minWidth;
} else if (percent) {
percentMinWidth = Math.max(percentMinWidth, Math.round(hint.minWidth/percent));
} else {
minWidth += hint.width;
}
// Build vertical margin sum
margin = child.getMarginTop() + child.getMarginBottom();
// Find biggest height
if ((hint.height+margin) > height) {
height = hint.height + margin;
}
// Find biggest minHeight
if ((hint.minHeight+margin) > minHeight) {
minHeight = hint.minHeight + margin;
}
}
minWidth += percentMinWidth;
// Respect gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
if (separator) {
var gaps = util.computeHorizontalSeparatorGaps(children, spacing, separator);
} else {
var gaps = util.computeHorizontalGaps(children, spacing, true);
}
// Return hint
return {
minWidth : minWidth + gaps,
width : width + gaps,
minHeight : minHeight,
height : height
};
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function() {
this.__widths = this.__flexs = this.__children = null;
}
});