@qooxdoo/framework
Version:
The JS Framework for Coders
1,049 lines (839 loc) • 25.7 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)
************************************************************************ */
/**
* The base class of all items, which should be laid out using a layout manager
* {@link qx.ui.layout.Abstract}.
*/
qx.Class.define("qx.ui.core.LayoutItem",
{
type : "abstract",
extend : qx.core.Object,
construct : function() {
this.base(arguments);
// dynamic theme switch
if (qx.core.Environment.get("qx.dyntheme")) {
qx.theme.manager.Meta.getInstance().addListener("changeTheme", this._onChangeTheme, this);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/*
---------------------------------------------------------------------------
DIMENSION
---------------------------------------------------------------------------
*/
/**
* The user provided minimal width.
*
* Also take a look at the related properties {@link #width} and {@link #maxWidth}.
*/
minWidth :
{
check : "Integer",
nullable : true,
apply : "_applyDimension",
init : null,
themeable : true
},
/**
* The <code>LayoutItem</code>'s preferred width.
*
* The computed width may differ from the given width due to
* stretching. Also take a look at the related properties
* {@link #minWidth} and {@link #maxWidth}.
*/
width :
{
check : "Integer",
event : "changeWidth",
nullable : true,
apply : "_applyDimension",
init : null,
themeable : true
},
/**
* The user provided maximal width.
*
* Also take a look at the related properties {@link #width} and {@link #minWidth}.
*/
maxWidth :
{
check : "Integer",
nullable : true,
apply : "_applyDimension",
init : null,
themeable : true
},
/**
* The user provided minimal height.
*
* Also take a look at the related properties {@link #height} and {@link #maxHeight}.
*/
minHeight :
{
check : "Integer",
nullable : true,
apply : "_applyDimension",
init : null,
themeable : true
},
/**
* The item's preferred height.
*
* The computed height may differ from the given height due to
* stretching. Also take a look at the related properties
* {@link #minHeight} and {@link #maxHeight}.
*/
height :
{
check : "Integer",
event : "changeHeight",
nullable : true,
apply : "_applyDimension",
init : null,
themeable : true
},
/**
* The user provided maximum height.
*
* Also take a look at the related properties {@link #height} and {@link #minHeight}.
*/
maxHeight :
{
check : "Integer",
nullable : true,
apply : "_applyDimension",
init : null,
themeable : true
},
/*
---------------------------------------------------------------------------
STRETCHING
---------------------------------------------------------------------------
*/
/** Whether the item can grow horizontally. */
allowGrowX :
{
check : "Boolean",
apply : "_applyStretching",
init : true,
themeable : true
},
/** Whether the item can shrink horizontally. */
allowShrinkX :
{
check : "Boolean",
apply : "_applyStretching",
init : true,
themeable : true
},
/** Whether the item can grow vertically. */
allowGrowY :
{
check : "Boolean",
apply : "_applyStretching",
init : true,
themeable : true
},
/** Whether the item can shrink vertically. */
allowShrinkY :
{
check : "Boolean",
apply : "_applyStretching",
init : true,
themeable : true
},
/** Growing and shrinking in the horizontal direction */
allowStretchX :
{
group : [ "allowGrowX", "allowShrinkX" ],
mode : "shorthand",
themeable: true
},
/** Growing and shrinking in the vertical direction */
allowStretchY :
{
group : [ "allowGrowY", "allowShrinkY" ],
mode : "shorthand",
themeable: true
},
/*
---------------------------------------------------------------------------
MARGIN
---------------------------------------------------------------------------
*/
/** Margin of the widget (top) */
marginTop :
{
check : "Integer",
init : 0,
apply : "_applyMargin",
themeable : true
},
/** Margin of the widget (right) */
marginRight :
{
check : "Integer",
init : 0,
apply : "_applyMargin",
themeable : true
},
/** Margin of the widget (bottom) */
marginBottom :
{
check : "Integer",
init : 0,
apply : "_applyMargin",
themeable : true
},
/** Margin of the widget (left) */
marginLeft :
{
check : "Integer",
init : 0,
apply : "_applyMargin",
themeable : true
},
/**
* The 'margin' property is a shorthand property for setting 'marginTop',
* 'marginRight', 'marginBottom' and 'marginLeft' at the same time.
*
* If four values are specified they apply to top, right, bottom and left respectively.
* If there is only one value, it applies to all sides, if there are two or three,
* the missing values are taken from the opposite side.
*/
margin :
{
group : [ "marginTop", "marginRight", "marginBottom", "marginLeft" ],
mode : "shorthand",
themeable : true
},
/*
---------------------------------------------------------------------------
ALIGN
---------------------------------------------------------------------------
*/
/**
* Horizontal alignment of the item in the parent layout.
*
* Note: Item alignment is only supported by {@link LayoutItem} layouts where
* it would have a visual effect. Except for {@link Spacer}, which provides
* blank space for layouts, all classes that inherit {@link LayoutItem} support alignment.
*/
alignX :
{
check : [ "left", "center", "right" ],
nullable : true,
apply : "_applyAlign",
themeable: true
},
/**
* Vertical alignment of the item in the parent layout.
*
* Note: Item alignment is only supported by {@link LayoutItem} layouts where
* it would have a visual effect. Except for {@link Spacer}, which provides
* blank space for layouts, all classes that inherit {@link LayoutItem} support alignment.
*/
alignY :
{
check : [ "top", "middle", "bottom", "baseline" ],
nullable : true,
apply : "_applyAlign",
themeable: true
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
/*
---------------------------------------------------------------------------
DYNAMIC THEME SWITCH SUPPORT
---------------------------------------------------------------------------
*/
/**
* Handler for the dynamic theme change.
* @signature function()
*/
_onChangeTheme : qx.core.Environment.select("qx.dyntheme",
{
"true" : function() {
// reset all themeable properties
var props = qx.util.PropertyUtil.getAllProperties(this.constructor);
for (var name in props) {
var desc = props[name];
// only themeable properties not having a user value
if (desc.themeable) {
var userValue = qx.util.PropertyUtil.getUserValue(this, name);
if (userValue == null) {
qx.util.PropertyUtil.resetThemed(this, name);
}
}
}
},
"false" : null
}),
/*
---------------------------------------------------------------------------
LAYOUT PROCESS
---------------------------------------------------------------------------
*/
/** @type {Integer} The computed height */
__computedHeightForWidth : null,
/** @type {Map} The computed size of the layout item */
__computedLayout : null,
/** @type {Boolean} Whether the current layout is valid */
__hasInvalidLayout : null,
/** @type {Map} Cached size hint */
__sizeHint : null,
/** @type {Boolean} Whether the margins have changed and must be updated */
__updateMargin : null,
/** @type {Map} user provided bounds of the widget, which override the layout manager */
__userBounds : null,
/** @type {Map} The item's layout properties */
__layoutProperties : null,
/**
* Get the computed location and dimension as computed by
* the layout manager.
*
* @return {Map|null} The location and dimensions in pixel
* (if the layout is valid). Contains the keys
* <code>width</code>, <code>height</code>, <code>left</code> and
* <code>top</code>.
*/
getBounds : function() {
return this.__userBounds || this.__computedLayout || null;
},
/**
* Reconfigure number of separators
*/
clearSeparators : function() {
// empty template
},
/**
* Renders a separator between two children
*
* @param separator {String|qx.ui.decoration.IDecorator} The separator to render
* @param bounds {Map} Contains the left and top coordinate and the width and height
* of the separator to render.
*/
renderSeparator : function(separator, bounds) {
// empty template
},
/**
* Used by the layout engine to apply coordinates and dimensions.
*
* @param left {Integer} Any integer value for the left position,
* always in pixels
* @param top {Integer} Any integer value for the top position,
* always in pixels
* @param width {Integer} Any positive integer value for the width,
* always in pixels
* @param height {Integer} Any positive integer value for the height,
* always in pixels
* @return {Map} A map of which layout sizes changed.
*/
renderLayout : function(left, top, width, height)
{
// do not render if the layout item is already disposed
if (this.isDisposed()) {
return null;
}
if (qx.core.Environment.get("qx.debug"))
{
var msg = "Something went wrong with the layout of " + this.toString() + "!";
this.assertInteger(left, "Wrong 'left' argument. " + msg);
this.assertInteger(top, "Wrong 'top' argument. " + msg);
this.assertInteger(width, "Wrong 'width' argument. " + msg);
this.assertInteger(height, "Wrong 'height' argument. " + msg);
// this.assertInRange(width, this.getMinWidth() || -1, this.getMaxWidth() || 32000);
// this.assertInRange(height, this.getMinHeight() || -1, this.getMaxHeight() || 32000);
}
// Height for width support
// Results into a relayout which means that width/height is applied in the next iteration.
var flowHeight = null;
if (this.getHeight() == null && this._hasHeightForWidth()) {
var flowHeight = this._getHeightForWidth(width);
}
if (flowHeight != null && flowHeight !== this.__computedHeightForWidth)
{
// This variable is used in the next computation of the size hint
this.__computedHeightForWidth = flowHeight;
// Re-add to layout queue
qx.ui.core.queue.Layout.add(this);
return null;
}
// Detect size changes
// Dynamically create data structure for computed layout
var computed = this.__computedLayout;
if (!computed) {
computed = this.__computedLayout = {};
}
// Detect changes
var changes = {};
if (left !== computed.left || top !== computed.top)
{
changes.position = true;
computed.left = left;
computed.top = top;
}
if (width !== computed.width || height !== computed.height)
{
changes.size = true;
computed.width = width;
computed.height = height;
}
// Clear invalidation marker
if (this.__hasInvalidLayout)
{
changes.local = true;
delete this.__hasInvalidLayout;
}
if (this.__updateMargin)
{
changes.margin = true;
delete this.__updateMargin;
}
// Returns changes, especially for deriving classes
return changes;
},
/**
* Whether the item should be excluded from the layout
*
* @return {Boolean} Should the item be excluded by the layout
*/
isExcluded : function() {
return false;
},
/**
* Whether the layout of this item (to layout the children)
* is valid.
*
* @return {Boolean} Returns <code>true</code>
*/
hasValidLayout : function() {
return !this.__hasInvalidLayout;
},
/**
* Indicate that the item has layout changes and propagate this information
* up the item hierarchy.
*
*/
scheduleLayoutUpdate : function() {
qx.ui.core.queue.Layout.add(this);
},
/**
* Called by the layout manager to mark this item's layout as invalid.
* This function should clear all layout relevant caches.
*/
invalidateLayoutCache : function()
{
// this.debug("Mark layout invalid!");
this.__hasInvalidLayout = true;
this.__sizeHint = null;
},
/**
* A size hint computes the dimensions of a widget. It returns
* the recommended dimensions as well as the min and max dimensions.
* The min and max values already respect the stretching properties.
*
* <h3>Wording</h3>
* <ul>
* <li>User value: Value defined by the widget user, using the size properties</li>
*
* <li>Layout value: The value computed by {@link qx.ui.core.Widget#_getContentHint}</li>
* </ul>
*
* <h3>Algorithm</h3>
* <ul>
* <li>minSize: If the user min size is not null, the user value is taken,
* otherwise the layout value is used.</li>
*
* <li>(preferred) size: If the user value is not null the user value is used,
* otherwise the layout value is used.</li>
*
* <li>max size: Same as the preferred size.</li>
* </ul>
*
* @param compute {Boolean?true} Automatically compute size hint if currently not
* cached?
* @return {Map} The map with the preferred width/height and the allowed
* minimum and maximum values in cases where shrinking or growing
* is required.
*/
getSizeHint : function(compute)
{
var hint = this.__sizeHint;
if (hint) {
return hint;
}
if (compute === false) {
return null;
}
// Compute as defined
hint = this.__sizeHint = this._computeSizeHint();
// Respect height for width
if (this._hasHeightForWidth() && this.__computedHeightForWidth && this.getHeight() == null) {
hint.height = this.__computedHeightForWidth;
}
// normalize width
if (hint.minWidth > hint.width) {
hint.width = hint.minWidth;
}
if (hint.maxWidth < hint.width) {
hint.width = hint.maxWidth;
}
if (!this.getAllowGrowX()) {
hint.maxWidth = hint.width;
}
if (!this.getAllowShrinkX()) {
hint.minWidth = hint.width;
}
// normalize height
if (hint.minHeight > hint.height) {
hint.height = hint.minHeight;
}
if (hint.maxHeight < hint.height) {
hint.height = hint.maxHeight;
}
if (!this.getAllowGrowY()) {
hint.maxHeight = hint.height;
}
if (!this.getAllowShrinkY()) {
hint.minHeight = hint.height;
}
// Finally return
return hint;
},
/**
* Computes the size hint of the layout item.
*
* @return {Map} The map with the preferred width/height and the allowed
* minimum and maximum values.
*/
_computeSizeHint : function()
{
var minWidth = this.getMinWidth() || 0;
var minHeight = this.getMinHeight() || 0;
var width = this.getWidth() || minWidth;
var height = this.getHeight() || minHeight;
var maxWidth = this.getMaxWidth() || Infinity;
var maxHeight = this.getMaxHeight() || Infinity;
return {
minWidth : minWidth,
width : width,
maxWidth : maxWidth,
minHeight : minHeight,
height : height,
maxHeight : maxHeight
};
},
/**
* Whether the item supports height for width.
*
* @return {Boolean} Whether the item supports height for width
*/
_hasHeightForWidth : function()
{
var layout = this._getLayout();
if (layout) {
return layout.hasHeightForWidth();
}
return false;
},
/**
* If an item wants to trade height for width it has to implement this
* method and return the preferred height of the item if it is resized to
* the given width. This function returns <code>null</code> if the item
* do not support height for width.
*
* @param width {Integer} The computed width
* @return {Integer} The desired height
*/
_getHeightForWidth : function(width)
{
var layout = this._getLayout();
if (layout && layout.hasHeightForWidth()) {
return layout.getHeightForWidth(width);
}
return null;
},
/**
* Get the widget's layout manager.
*
* @return {qx.ui.layout.Abstract} The widget's layout manager
*/
_getLayout : function() {
return null;
},
// property apply
_applyMargin : function()
{
this.__updateMargin = true;
var parent = this.$$parent;
if (parent) {
parent.updateLayoutProperties();
}
},
// property apply
_applyAlign : function()
{
var parent = this.$$parent;
if (parent) {
parent.updateLayoutProperties();
}
},
// property apply
_applyDimension : function() {
qx.ui.core.queue.Layout.add(this);
},
// property apply
_applyStretching : function() {
qx.ui.core.queue.Layout.add(this);
},
/*
---------------------------------------------------------------------------
SUPPORT FOR USER BOUNDARIES
---------------------------------------------------------------------------
*/
/**
* Whether user bounds are set on this layout item
*
* @return {Boolean} Whether user bounds are set on this layout item
*/
hasUserBounds : function() {
return !!this.__userBounds;
},
/**
* Set user bounds of the widget. Widgets with user bounds are sized and
* positioned manually and are ignored by any layout manager.
*
* @param left {Integer} left position (relative to the parent)
* @param top {Integer} top position (relative to the parent)
* @param width {Integer} width of the layout item
* @param height {Integer} height of the layout item
*/
setUserBounds : function(left, top, width, height)
{
this.__userBounds = {
left: left,
top: top,
width: width,
height: height
};
qx.ui.core.queue.Layout.add(this);
},
/**
* Clear the user bounds. After this call the layout item is laid out by
* the layout manager again.
*
*/
resetUserBounds : function()
{
delete this.__userBounds;
qx.ui.core.queue.Layout.add(this);
},
/*
---------------------------------------------------------------------------
LAYOUT PROPERTIES
---------------------------------------------------------------------------
*/
/**
* @type {Map} Empty storage pool
*
* @lint ignoreReferenceField(__emptyProperties)
*/
__emptyProperties : {},
/**
* Stores the given layout properties
*
* @param props {Map} Incoming layout property data
*/
setLayoutProperties : function(props)
{
if (props == null) {
return;
}
var storage = this.__layoutProperties;
if (!storage) {
storage = this.__layoutProperties = {};
}
// Check values through parent
var parent = this.getLayoutParent();
if (parent) {
parent.updateLayoutProperties(props);
}
// Copy over values
for (var key in props)
{
if (props[key] == null) {
delete storage[key];
} else {
storage[key] = props[key];
}
}
},
/**
* Returns currently stored layout properties
*
* @return {Map} Returns a map of layout properties
*/
getLayoutProperties : function() {
return this.__layoutProperties || this.__emptyProperties;
},
/**
* Removes all stored layout properties.
*
*/
clearLayoutProperties : function() {
delete this.__layoutProperties;
},
/**
* Should be executed on every change of layout properties.
*
* This also includes "virtual" layout properties like margin or align
* when they have an effect on the parent and not on the widget itself.
*
* This method is always executed on the parent not on the
* modified widget itself.
*
* @param props {Map?null} Optional map of known layout properties
*/
updateLayoutProperties : function(props)
{
var layout = this._getLayout();
if (layout)
{
// Verify values through underlying layout
if (qx.core.Environment.get("qx.debug"))
{
if (props)
{
for (var key in props) {
if (props[key] !== null) {
layout.verifyLayoutProperty(this, key, props[key]);
}
}
}
}
// Precomputed and cached children data need to be
// rebuild on upcoming (re-)layout.
layout.invalidateChildrenCache();
}
qx.ui.core.queue.Layout.add(this);
},
/*
---------------------------------------------------------------------------
HIERARCHY SUPPORT
---------------------------------------------------------------------------
*/
/**
* Returns the application root
*
* @return {qx.ui.root.Abstract} The currently used root
*/
getApplicationRoot : function() {
return qx.core.Init.getApplication().getRoot();
},
/**
* Get the items parent. Even if the item has been added to a
* layout, the parent is always a child of the containing item. The parent
* item may be <code>null</code>.
*
* @return {qx.ui.core.Widget|null} The parent.
*/
getLayoutParent : function() {
return this.$$parent || null;
},
/**
* Set the parent
*
* @param parent {qx.ui.core.Widget|null} The new parent.
*/
setLayoutParent : function(parent)
{
if (this.$$parent === parent) {
return;
}
this.$$parent = parent || null;
qx.ui.core.queue.Visibility.add(this);
},
/**
* Whether the item is a root item and directly connected to
* the DOM.
*
* @return {Boolean} Whether the item a root item
*/
isRootWidget : function() {
return false;
},
/**
* Returns the root item. The root item is the item which
* is directly inserted into an existing DOM node at HTML level.
* This is often the BODY element of a typical web page.
*
* @return {qx.ui.core.Widget} The root item (if available)
*/
_getRoot : function()
{
var parent = this;
while (parent)
{
if (parent.isRootWidget()) {
return parent;
}
parent = parent.$$parent;
}
return null;
},
/*
---------------------------------------------------------------------------
CLONE SUPPORT
---------------------------------------------------------------------------
*/
// overridden
clone : function()
{
var clone = this.base(arguments);
var props = this.__layoutProperties;
if (props) {
clone.__layoutProperties = qx.lang.Object.clone(props);
}
return clone;
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
// remove dynamic theme listener
if (qx.core.Environment.get("qx.dyntheme")) {
qx.theme.manager.Meta.getInstance().removeListener(
"changeTheme", this._onChangeTheme, this
);
}
this.$$parent = this.$$subparent = this.__layoutProperties =
this.__computedLayout = this.__userBounds = this.__sizeHint = null;
}
});