@qooxdoo/framework
Version:
The JS Framework for Coders
939 lines (755 loc) • 26 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)
************************************************************************ */
/**
* Docks children to one of the edges.
*
* *Features*
*
* * Percent width for left/right/center attached children
* * Percent height for top/bottom/center attached children
* * Minimum and maximum dimensions
* * Prioritized growing/shrinking (flex)
* * Auto sizing
* * Margins and Spacings
* * Alignment in orthogonal axis (e.g. alignX of north attached)
* * Different sort options for children
*
* *Item Properties*
*
* <ul>
* <li><strong>edge</strong> <em>(String)</em>: The edge where the layout item
* should be docked. This may be one of <code>north</code>, <code>east</code>,
* <code>south</code>, <code>west</code> or <code>center</code>. (Required)</li>
* <li><strong>width</strong> <em>(String)</em>: Defines a percent
* width for the item. The percent width,
* when specified, is used instead of the width defined by the size hint.
* This is only supported for children added to the north or south edge or
* are centered in the middle of the layout.
* The minimum and maximum width still takes care of the elements limitations.
* It has no influence on the layout's size hint. Percents are mainly useful for
* widgets which are sized by the outer hierarchy.
* </li>
* <li><strong>height</strong> <em>(String)</em>: Defines a percent
* height for the item. The percent height,
* when specified, is used instead of the height defined by the size hint.
* This is only supported for children added to the west or east edge or
* are centered in the middle of the layout.
* The minimum and maximum height still takes care of the elements limitations.
* It has no influence on the layout's size hint. Percents are mainly useful for
* widgets which are sized by the outer hierarchy.
* </li>
* </ul>
*
* *Example*
*
* <pre class="javascript">
* var layout = new qx.ui.layout.Dock();
*
* var w1 = new qx.ui.core.Widget();
* var w2 = new qx.ui.core.Widget();
* var w3 = new qx.ui.core.Widget();
*
* w1.setHeight(200);
* w2.setWidth(150);
*
* var container = new qx.ui.container.Composite(layout);
* container.add(w1, {edge:"north"});
* container.add(w2, {edge:"west"});
* container.add(w3, {edge:"center"});
* </pre>
*
* *Detailed Description*
*
* Using this layout, items may be "docked" to a specific side
* of the available space. Each displayed item reduces the available space
* for the following children. Priorities depend on the position of
* the child in the internal children list.
*
* *External Documentation*
*
* <a href='http://manual.qooxdoo.org/${qxversion}/pages/layout/dock.html'>
* Extended documentation</a> and links to demos of this layout in the qooxdoo manual.
*/
qx.Class.define("qx.ui.layout.Dock",
{
extend : qx.ui.layout.Abstract,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param spacingX {Integer?0} The horizontal spacing. Sets {@link #spacingX}.
* @param spacingY {Integer?0} The vertical spacing. Sets {@link #spacingY}.
* @param separatorX {String|qx.ui.decoration.IDecorator} Separator to render between columns
* @param separatorY {String|qx.ui.decoration.IDecorator} Separator to render between rows
*/
construct : function(spacingX, spacingY, separatorX, separatorY)
{
this.base(arguments);
if (spacingX) {
this.setSpacingX(spacingX);
}
if (spacingY) {
this.setSpacingY(spacingY);
}
if (separatorX) {
this.setSeparatorX(separatorX);
}
if (separatorY) {
this.setSeparatorY(separatorY);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/**
* The way the widgets should be displayed (in conjunction with their
* position in the childrens array).
*/
sort :
{
check : [ "auto", "y", "x" ],
init : "auto",
apply : "_applySort"
},
/** Separator lines to use between the horizontal objects */
separatorX :
{
check : "Decorator",
nullable : true,
apply : "_applyLayoutChange"
},
/** Separator lines to use between the vertical objects */
separatorY :
{
check : "Decorator",
nullable : true,
apply : "_applyLayoutChange"
},
/**
* Whether separators should be collapsed so when a spacing is
* configured the line go over into each other
*/
connectSeparators :
{
check : "Boolean",
init : false,
apply : "_applyLayoutChange"
},
/** Horizontal spacing between two children */
spacingX :
{
check : "Integer",
init : 0,
apply : "_applyLayoutChange"
},
/** Vertical spacing between two children */
spacingY :
{
check : "Integer",
init : 0,
apply : "_applyLayoutChange"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__children : null,
__edges : null,
// overridden
verifyLayoutProperty : qx.core.Environment.select("qx.debug",
{
"true" : function(item, name, value)
{
this.assertInArray(name, ["flex", "edge", "height", "width"], "The property '"+name+"' is not supported by the Dock layout!");
if (name === "edge")
{
this.assertInArray(value, ["north", "south", "west", "east", "center"]);
}
else if (name === "flex")
{
this.assertNumber(value);
this.assert(value >= 0);
} else {
this.assertMatch(value, qx.ui.layout.Util.PERCENT_VALUE);
}
},
"false" : null
}),
// property apply
_applySort : function()
{
// easiest way is to invalidate the cache
this._invalidChildrenCache = true;
// call normal layout change
this._applyLayoutChange();
},
/**
* @type {Map} Maps edge IDs to numeric values
*
* @lint ignoreReferenceField(__edgeMap)
*/
__edgeMap :
{
north : 1,
south : 2,
west : 3,
east : 4,
center : 5
},
/**
* @type {Map} Maps edges to align values
*
* @lint ignoreReferenceField(__alignMap)
*/
__alignMap :
{
1 : "top",
2 : "bottom",
3 : "left",
4 : "right"
},
/**
* Rebuilds cache for sorted children list.
*
*/
__rebuildCache : function()
{
var all = this._getLayoutChildren();
var child, center;
var length = all.length;
var high = [];
var low = [];
var edge = [];
var yfirst = this.getSort() === "y";
var xfirst = this.getSort() === "x";
for (var i=0; i<length; i++)
{
child = all[i];
edge = child.getLayoutProperties().edge;
if (edge === "center")
{
if (center) {
throw new Error("It is not allowed to have more than one child aligned to 'center'!");
}
center = child;
}
else if (xfirst || yfirst)
{
if (edge === "north" || edge === "south") {
yfirst ? high.push(child) : low.push(child);
} else if (edge === "west" || edge === "east") {
yfirst ? low.push(child) : high.push(child);
}
}
else
{
high.push(child);
}
}
// Combine sorted children list
var result = high.concat(low);
if (center) {
result.push(center);
}
this.__children = result;
// Cache edges for faster access
var edges=[];
for (var i=0; i<length; i++)
{
edge = result[i].getLayoutProperties().edge;
edges[i] = this.__edgeMap[edge] || 5;
}
this.__edges = edges;
// Clear invalidation marker
delete this._invalidChildrenCache;
},
/*
---------------------------------------------------------------------------
LAYOUT INTERFACE
---------------------------------------------------------------------------
*/
// overridden
renderLayout : function(availWidth, availHeight, padding)
{
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var util = qx.ui.layout.Util;
var children = this.__children;
var edges = this.__edges;
var length = children.length;
var flexibles, child, hint, props, flex, grow, width, height, offset;
var widths = [];
var heights = [];
var separatorWidths = this._getSeparatorWidths();
var spacingX = this.getSpacingX();
var spacingY = this.getSpacingY();
// **************************************
// Caching children data
// **************************************
var allocatedWidth = -spacingX;
var allocatedHeight = -spacingY;
if (separatorWidths.x) {
allocatedWidth -= separatorWidths.x + spacingX;
}
if (separatorWidths.y) {
allocatedHeight -= separatorWidths.y + spacingY;
}
for (var i=0; i<length; i++)
{
child = children[i];
props = child.getLayoutProperties();
hint = child.getSizeHint();
width = hint.width;
height = hint.height;
if (props.width != null)
{
width = Math.floor(availWidth * parseFloat(props.width) / 100);
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
}
if (props.height != null)
{
height = Math.floor(availHeight * parseFloat(props.height) / 100);
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
}
widths[i] = width;
heights[i] = height;
// Update allocated width
switch(edges[i])
{
// north+south
case 1:
case 2:
allocatedHeight += height + child.getMarginTop() + child.getMarginBottom() + spacingY;
if (separatorWidths.y) {
allocatedHeight += separatorWidths.y + spacingY;
}
break;
// west+east
case 3:
case 4:
allocatedWidth += width + child.getMarginLeft() + child.getMarginRight() + spacingX;
if (separatorWidths.x) {
allocatedWidth += separatorWidths.x + spacingX;
}
break;
// center
default:
allocatedWidth += width + child.getMarginLeft() + child.getMarginRight() + spacingX;
allocatedHeight += height + child.getMarginTop() + child.getMarginBottom() + spacingY;
if (separatorWidths.x) {
allocatedWidth += separatorWidths.x + spacingX;
}
if (separatorWidths.y) {
allocatedHeight += separatorWidths.y + spacingY;
}
}
}
// **************************************
// Horizontal flex support
// **************************************
if (allocatedWidth != availWidth)
{
flexibles = {};
grow = allocatedWidth < availWidth;
for (var i=0; i<length; i++)
{
child = children[i];
switch(edges[i])
{
case 3:
case 4:
case 5:
flex = child.getLayoutProperties().flex;
// Default flex for centered children is '1'
if (flex == null && edges[i] == 5) {
flex = 1;
}
if (flex > 0)
{
hint = child.getSizeHint();
flexibles[i] =
{
min : hint.minWidth,
value : widths[i],
max : hint.maxWidth,
flex : flex
};
}
}
}
var result = util.computeFlexOffsets(flexibles, availWidth, allocatedWidth);
for (var i in result)
{
offset = result[i].offset;
widths[i] += offset;
allocatedWidth += offset;
}
}
// **************************************
// Vertical flex support
// **************************************
// Process height for flex stretching/shrinking
if (allocatedHeight != availHeight)
{
flexibles = {};
grow = allocatedHeight < availHeight;
for (var i=0; i<length; i++)
{
child = children[i];
switch(edges[i])
{
case 1:
case 2:
case 5:
flex = child.getLayoutProperties().flex;
// Default flex for centered children is '1'
if (flex == null && edges[i] == 5) {
flex = 1;
}
if (flex > 0)
{
hint = child.getSizeHint();
flexibles[i] =
{
min : hint.minHeight,
value : heights[i],
max : hint.maxHeight,
flex : flex
};
}
}
}
var result = util.computeFlexOffsets(flexibles, availHeight, allocatedHeight);
for (var i in result)
{
offset = result[i].offset;
heights[i] += offset;
allocatedHeight += offset;
}
}
// **************************************
// Layout children
// **************************************
// Pre configure separators
this._clearSeparators();
// Prepare loop
var separatorX=this.getSeparatorX(), separatorY=this.getSeparatorY();
var connectSeparators=this.getConnectSeparators();
var nextTop=0, nextLeft=0;
var left, top, width, height, used, edge;
var separatorLeft, separatorTop, separatorWidth, separatorHeight;
var marginTop, marginBottom, marginLeft, marginRight;
var alignMap = this.__alignMap;
for (var i=0; i<length; i++)
{
// Cache child data
child = children[i];
edge = edges[i];
hint = child.getSizeHint();
// Cache child margins
marginTop = child.getMarginTop();
marginBottom = child.getMarginBottom();
marginLeft = child.getMarginLeft();
marginRight = child.getMarginRight();
// Calculate child layout
switch(edge)
{
// north + south
case 1:
case 2:
// Full available width
width = availWidth - marginLeft - marginRight;
// Limit width to min/max
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
// Child preferred height
height = heights[i];
// Compute position
top = nextTop + util.computeVerticalAlignOffset(alignMap[edge], height, availHeight, marginTop, marginBottom);
left = nextLeft + util.computeHorizontalAlignOffset(child.getAlignX()||"left", width, availWidth, marginLeft, marginRight);
// Render the separator
if (separatorWidths.y)
{
if (edge == 1) {
separatorTop = nextTop + height + marginTop + spacingY + marginBottom;
} else {
separatorTop = nextTop + availHeight - height - marginTop - spacingY - marginBottom - separatorWidths.y;
}
separatorLeft = left;
separatorWidth = availWidth;
if (connectSeparators && separatorLeft > 0)
{
separatorLeft -= spacingX + marginLeft;
separatorWidth += (spacingX) * 2;
}
else
{
separatorLeft -= marginLeft;
}
this._renderSeparator(separatorY, {
left : separatorLeft + padding.left,
top : separatorTop + padding.top,
width : separatorWidth,
height : separatorWidths.y
});
}
// Update available height
used = height + marginTop + marginBottom + spacingY;
if (separatorWidths.y) {
used += separatorWidths.y + spacingY;
}
availHeight -= used;
// Update coordinates, for next child
if (edge == 1) {
nextTop += used;
}
break;
// west + east
case 3:
case 4:
// Full available height
height = availHeight - marginTop - marginBottom;
// Limit height to min/max
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
// Child preferred width
width = widths[i];
// Compute position
left = nextLeft + util.computeHorizontalAlignOffset(alignMap[edge], width, availWidth, marginLeft, marginRight);
top = nextTop + util.computeVerticalAlignOffset(child.getAlignY()||"top", height, availHeight, marginTop, marginBottom);
// Render the separator
if (separatorWidths.x)
{
if (edge == 3) {
separatorLeft = nextLeft + width + marginLeft + spacingX + marginRight;
} else {
separatorLeft = nextLeft + availWidth - width - marginLeft - spacingX - marginRight - separatorWidths.x;
}
separatorTop = top;
separatorHeight = availHeight;
if (connectSeparators && separatorTop > 0)
{
separatorTop -= spacingY + marginTop;
separatorHeight += (spacingY) * 2;
}
else
{
separatorTop -= marginTop;
}
this._renderSeparator(separatorX, {
left : separatorLeft + padding.left,
top : separatorTop + padding.top,
width : separatorWidths.x,
height : separatorHeight
});
}
// Update available height
used = width + marginLeft + marginRight + spacingX;
if (separatorWidths.x) {
used += separatorWidths.x + spacingX;
}
availWidth -= used;
// Update coordinates, for next child
if (edge == 3) {
nextLeft += used;
}
break;
// center
default:
// Calculated width/height
width = availWidth - marginLeft - marginRight;
height = availHeight - marginTop - marginBottom;
// Limit width to min/max
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
// Limit height to min/max
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
// Compute coordinates (respect margins and alignments for both axis)
left = nextLeft + util.computeHorizontalAlignOffset(child.getAlignX()||"left", width, availWidth, marginLeft, marginRight);
top = nextTop + util.computeVerticalAlignOffset(child.getAlignY()||"top", height, availHeight, marginTop, marginBottom);
}
// Apply layout
child.renderLayout(left + padding.left, top + padding.top, width, height);
}
},
/**
* Computes the dimensions each separator on both the <code>x</code> and
* <code>y</code> axis needs.
*
* @return {Map} Map with the keys <code>x</code> and
* <code>y</code>
*/
_getSeparatorWidths : function()
{
var separatorX=this.getSeparatorX(), separatorY=this.getSeparatorY();
if (separatorX || separatorY) {
var decorationManager = qx.theme.manager.Decoration.getInstance();
}
if (separatorX)
{
var separatorInstanceX = decorationManager.resolve(separatorX);
var separatorInsetsX = separatorInstanceX.getInsets();
var separatorWidthX = separatorInsetsX.left + separatorInsetsX.right;
}
if (separatorY)
{
var separatorInstanceY = decorationManager.resolve(separatorY);
var separatorInsetsY = separatorInstanceY.getInsets();
var separatorWidthY = separatorInsetsY.top + separatorInsetsY.bottom;
}
return {
x : separatorWidthX || 0,
y : separatorWidthY || 0
};
},
// overridden
_computeSizeHint : function()
{
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var children = this.__children;
var edges = this.__edges;
var length = children.length;
var hint, child;
var marginX, marginY;
var widthX=0, minWidthX=0;
var heightX=0, minHeightX=0;
var widthY=0, minWidthY=0;
var heightY=0, minHeightY=0;
var separatorWidths = this._getSeparatorWidths();
var spacingX=this.getSpacingX(), spacingY=this.getSpacingY();
var spacingSumX=-spacingX, spacingSumY=-spacingY;
if (separatorWidths.x) {
spacingSumX -= separatorWidths.x + spacingX;
}
if (separatorWidths.y) {
spacingSumY -= separatorWidths.y + spacingY;
}
// Detect children sizes
for (var i=0; i<length; i++)
{
child = children[i];
hint = child.getSizeHint();
// Pre-cache margin sums
marginX = child.getMarginLeft() + child.getMarginRight();
marginY = child.getMarginTop() + child.getMarginBottom();
// Ok, this part is a bit complicated :)
switch(edges[i])
{
case 1:
case 2:
// Find the maximum width used by these fully stretched items
// The recommended width used by these must add the currently
// occupied width by the orthogonal ordered children.
widthY = Math.max(widthY, hint.width + widthX + marginX);
minWidthY = Math.max(minWidthY, hint.minWidth + minWidthX + marginX);
// Add the needed heights of this widget
heightY += hint.height + marginY;
minHeightY += hint.minHeight + marginY;
// Add spacing
spacingSumY += spacingY;
if (separatorWidths.y) {
spacingSumY += separatorWidths.y + spacingY;
}
break;
case 3:
case 4:
// Find the maximum height used by these fully stretched items
// The recommended height used by these must add the currently
// occupied height by the orthogonal ordered children.
heightX = Math.max(heightX, hint.height + heightY + marginY);
minHeightX = Math.max(minHeightX, hint.minHeight + minHeightY + marginY);
// Add the needed widths of this widget
widthX += hint.width + marginX;
minWidthX += hint.minWidth + marginX;
// Add spacing
spacingSumX += spacingX;
if (separatorWidths.x) {
spacingSumX += separatorWidths.x + spacingX;
}
break;
default:
// A centered widget must be added to both sums as
// it stretches into the remaining available space.
widthX += hint.width + marginX;
minWidthX += hint.minWidth + marginX;
heightY += hint.height + marginY;
minHeightY += hint.minHeight + marginY;
// Add spacing
spacingSumX += spacingX;
if (separatorWidths.x) {
spacingSumX += separatorWidths.x + spacingX;
}
spacingSumY += spacingY;
if (separatorWidths.y) {
spacingSumY += separatorWidths.y + spacingY;
}
}
}
var minWidth = Math.max(minWidthX, minWidthY) + spacingSumX;
var width = Math.max(widthX, widthY) + spacingSumX;
var minHeight = Math.max(minHeightX, minHeightY) + spacingSumY;
var height = Math.max(heightX, heightY) + spacingSumY;
// Return hint
return {
minWidth : minWidth,
width : width,
minHeight : minHeight,
height : height
};
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function() {
this.__edges = this.__children = null;
}
});