UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,438 lines (1,226 loc) 42.8 kB
/* ************************************************************************ 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 grid layout manager arranges the items in a two dimensional * grid. Widgets can be placed into the grid's cells and may span multiple rows * and columns. * * *Features* * * * Flex values for rows and columns * * Minimal and maximal column and row sizes * * Manually setting of column and row sizes * * Horizontal and vertical alignment * * Horizontal and vertical spacing * * Column and row spans * * Auto-sizing * * *Item Properties* * * <ul> * <li><strong>row</strong> <em>(Integer)</em>: The row of the cell the * widget should occupy. Each cell can only containing one widget. This layout * property is mandatory. * </li> * <li><strong>column</strong> <em>(Integer)</em>: The column of the cell the * widget should occupy. Each cell can only containing one widget. This layout * property is mandatory. * </li> * <li><strong>rowSpan</strong> <em>(Integer)</em>: The number of rows, the * widget should span, starting from the row specified in the <code>row</code> * property. The cells in the spanned rows must be empty as well. * </li> * <li><strong>colSpan</strong> <em>(Integer)</em>: The number of columns, the * widget should span, starting from the column specified in the <code>column</code> * property. The cells in the spanned columns must be empty as well. * </li> * </ul> * * *Example* * * Here is a little example of how to use the grid layout. * * <pre class="javascript"> * var layout = new qx.ui.layout.Grid(); * layout.setRowFlex(0, 1); // make row 0 flexible * layout.setColumnWidth(1, 200); // set with of column 1 to 200 pixel * * var container = new qx.ui.container.Composite(layout); * container.add(new qx.ui.core.Widget(), {row: 0, column: 0}); * container.add(new qx.ui.core.Widget(), {row: 0, column: 1}); * container.add(new qx.ui.core.Widget(), {row: 1, column: 0, rowSpan: 2}); * </pre> * * *External Documentation* * * <a href='https://qooxdoo.org/documentation/#/desktop/layout/grid.md'> * Extended documentation</a> and links to demos of this layout in the qooxdoo manual. */ qx.Class.define("qx.ui.layout.Grid", { extend: qx.ui.layout.Abstract, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param spacingX {Integer?0} The horizontal spacing between grid cells. * Sets {@link #spacingX}. * @param spacingY {Integer?0} The vertical spacing between grid cells. * Sets {@link #spacingY}. */ construct(spacingX, spacingY) { super(); this.__rowData = []; this.__colData = []; if (spacingX) { this.setSpacingX(spacingX); } if (spacingY) { this.setSpacingY(spacingY); } }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties: { /** * The horizontal spacing between grid cells. */ spacingX: { check: "Integer", init: 0, apply: "_applyLayoutChange" }, /** * The vertical spacing between grid cells. */ spacingY: { check: "Integer", init: 0, apply: "_applyLayoutChange" }, /** * Allow growing of spanning cells' widths beyond the accumulated widths of the columns. * The default behavior (init value false) is that the width of the spanning cell is * determined by the accumulated width of the columns (plus spacing). * Setting this property to true lets the cell width grow as needed to show * the widget in the spanning cell, which also enlarges the width of the spanned columns. */ allowGrowSpannedCellWidth: { check: "Boolean", init: false, apply: "_applyLayoutChange" } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members: { /** @type {Array} 2D array of grid cell data */ __grid: null, __rowData: null, __colData: null, __colSpans: null, __rowSpans: null, __maxRowIndex: null, __maxColIndex: null, /** @type {Array} cached row heights */ __rowHeights: null, /** @type {Array} cached column widths */ __colWidths: null, // overridden verifyLayoutProperty: qx.core.Environment.select("qx.debug", { true(item, name, value) { var layoutProperties = { row: 1, column: 1, rowSpan: 1, colSpan: 1 }; this.assert( layoutProperties[name] == 1, "The property '" + name + "' is not supported by the Grid layout!" ); this.assertInteger(value); this.assert(value >= 0, "Value must be positive"); }, false: null }), /** * Rebuild the internal representation of the grid */ __buildGrid() { var grid = []; var colSpans = []; var rowSpans = []; var maxRowIndex = -1; var maxColIndex = -1; var children = this._getLayoutChildren(); for (var i = 0, l = children.length; i < l; i++) { var child = children[i]; var props = child.getLayoutProperties(); var row = props.row; var column = props.column; props.colSpan = props.colSpan || 1; props.rowSpan = props.rowSpan || 1; // validate arguments if (row == null || column == null) { throw new Error( "The layout properties 'row' and 'column' of the child widget '" + child + "' must be defined!" ); } if (grid[row] && grid[row][column]) { throw new Error( "Cannot add widget '" + child + "'!. " + "There is already a widget '" + grid[row][column] + "' in this cell (" + row + ", " + column + ") for '" + this + "'" ); } for (var x = column; x < column + props.colSpan; x++) { for (var y = row; y < row + props.rowSpan; y++) { if (grid[y] == undefined) { grid[y] = []; } grid[y][x] = child; maxColIndex = Math.max(maxColIndex, x); maxRowIndex = Math.max(maxRowIndex, y); } } if (props.rowSpan > 1) { rowSpans.push(child); } if (props.colSpan > 1) { colSpans.push(child); } } // make sure all columns are defined so that accessing the grid using // this.__grid[column][row] will never raise an exception for (var y = 0; y <= maxRowIndex; y++) { if (grid[y] == undefined) { grid[y] = []; } } this.__grid = grid; this.__colSpans = colSpans; this.__rowSpans = rowSpans; this.__maxRowIndex = maxRowIndex; this.__maxColIndex = maxColIndex; this.__rowHeights = null; this.__colWidths = null; // Clear invalidation marker delete this._invalidChildrenCache; }, /** * Stores data for a grid row * * @param row {Integer} The row index * @param key {String} The key under which the data should be stored * @param value {var} data to store */ _setRowData(row, key, value) { var rowData = this.__rowData[row]; if (!rowData) { this.__rowData[row] = {}; this.__rowData[row][key] = value; } else { rowData[key] = value; } }, /** * Stores data for a grid column * * @param column {Integer} The column index * @param key {String} The key under which the data should be stored * @param value {var} data to store */ _setColumnData(column, key, value) { var colData = this.__colData[column]; if (!colData) { this.__colData[column] = {}; this.__colData[column][key] = value; } else { colData[key] = value; } }, /** * Shortcut to set both horizontal and vertical spacing between grid cells * to the same value. * * @param spacing {Integer} new horizontal and vertical spacing * @return {qx.ui.layout.Grid} This object (for chaining support). */ setSpacing(spacing) { this.setSpacingY(spacing); this.setSpacingX(spacing); return this; }, /** * Set the default cell alignment for a column. This alignment can be * overridden on a per cell basis by setting the cell's content widget's * <code>alignX</code> and <code>alignY</code> properties. * * If on a grid cell both row and a column alignment is set, the horizontal * alignment is taken from the column and the vertical alignment is taken * from the row. * * @param column {Integer} Column index * @param hAlign {String} The horizontal alignment. Valid values are * "left", "center" and "right". * @param vAlign {String} The vertical alignment. Valid values are * "top", "middle", "bottom" * @return {qx.ui.layout.Grid} This object (for chaining support) */ setColumnAlign(column, hAlign, vAlign) { if (qx.core.Environment.get("qx.debug")) { this.assertInteger(column, "Invalid parameter 'column'"); this.assertInArray(hAlign, ["left", "center", "right"]); this.assertInArray(vAlign, ["top", "middle", "bottom"]); } this._setColumnData(column, "hAlign", hAlign); this._setColumnData(column, "vAlign", vAlign); this._applyLayoutChange(); return this; }, /** * Get a map of the column's alignment. * * @param column {Integer} The column index * @return {Map} A map with the keys <code>vAlign</code> and <code>hAlign</code> * containing the vertical and horizontal column alignment. */ getColumnAlign(column) { var colData = this.__colData[column] || {}; return { vAlign: colData.vAlign || "top", hAlign: colData.hAlign || "left" }; }, /** * Set the default cell alignment for a row. This alignment can be * overridden on a per cell basis by setting the cell's content widget's * <code>alignX</code> and <code>alignY</code> properties. * * If on a grid cell both row and a column alignment is set, the horizontal * alignment is taken from the column and the vertical alignment is taken * from the row. * * @param row {Integer} Row index * @param hAlign {String} The horizontal alignment. Valid values are * "left", "center" and "right". * @param vAlign {String} The vertical alignment. Valid values are * "top", "middle", "bottom" * @return {qx.ui.layout.Grid} This object (for chaining support) */ setRowAlign(row, hAlign, vAlign) { if (qx.core.Environment.get("qx.debug")) { this.assertInteger(row, "Invalid parameter 'row'"); this.assertInArray(hAlign, ["left", "center", "right"]); this.assertInArray(vAlign, ["top", "middle", "bottom"]); } this._setRowData(row, "hAlign", hAlign); this._setRowData(row, "vAlign", vAlign); this._applyLayoutChange(); return this; }, /** * Get a map of the row's alignment. * * @param row {Integer} The Row index * @return {Map} A map with the keys <code>vAlign</code> and <code>hAlign</code> * containing the vertical and horizontal row alignment. */ getRowAlign(row) { var rowData = this.__rowData[row] || {}; return { vAlign: rowData.vAlign || "top", hAlign: rowData.hAlign || "left" }; }, /** * Get the widget located in the cell. If a the cell is empty or the widget * has a {@link qx.ui.core.Widget#visibility} value of <code>exclude</code>, * <code>null</code> is returned. * * @param row {Integer} The cell's row index * @param column {Integer} The cell's column index * @return {qx.ui.core.Widget|null}The cell's widget. The value may be null. */ getCellWidget(row, column) { if (this._invalidChildrenCache) { this.__buildGrid(); } var row = this.__grid[row] || {}; return row[column] || null; }, /** * Get the number of rows in the grid layout. * * @return {Integer} The number of rows in the layout */ getRowCount() { if (this._invalidChildrenCache) { this.__buildGrid(); } return this.__maxRowIndex + 1; }, /** * Get the number of columns in the grid layout. * * @return {Integer} The number of columns in the layout */ getColumnCount() { if (this._invalidChildrenCache) { this.__buildGrid(); } return this.__maxColIndex + 1; }, /** * Get a map of the cell's alignment. For vertical alignment the row alignment * takes precedence over the column alignment. For horizontal alignment it is * the over way round. If an alignment is set on the cell widget using * {@link qx.ui.core.LayoutItem#setLayoutProperties}, this alignment takes * always precedence over row or column alignment. * * @param row {Integer} The cell's row index * @param column {Integer} The cell's column index * @return {Map} A map with the keys <code>vAlign</code> and <code>hAlign</code> * containing the vertical and horizontal cell alignment. */ getCellAlign(row, column) { var vAlign = "top"; var hAlign = "left"; var rowData = this.__rowData[row]; var colData = this.__colData[column]; var widget = this.__grid[row][column]; if (widget) { var widgetProps = { vAlign: widget.getAlignY(), hAlign: widget.getAlignX() }; } else { widgetProps = {}; } // compute vAlign // precedence : widget -> row -> column if (widgetProps.vAlign) { vAlign = widgetProps.vAlign; } else if (rowData && rowData.vAlign) { vAlign = rowData.vAlign; } else if (colData && colData.vAlign) { vAlign = colData.vAlign; } // compute hAlign // precedence : widget -> column -> row if (widgetProps.hAlign) { hAlign = widgetProps.hAlign; } else if (colData && colData.hAlign) { hAlign = colData.hAlign; } else if (rowData && rowData.hAlign) { hAlign = rowData.hAlign; } return { vAlign: vAlign, hAlign: hAlign }; }, /** * Set the flex value for a grid column. * By default the column flex value is <code>0</code>. * * @param column {Integer} The column index * @param flex {Integer} The column's flex value * @return {qx.ui.layout.Grid} This object (for chaining support) */ setColumnFlex(column, flex) { this._setColumnData(column, "flex", flex); this._applyLayoutChange(); return this; }, /** * Get the flex value of a grid column. * * @param column {Integer} The column index * @return {Integer} The column's flex value */ getColumnFlex(column) { var colData = this.__colData[column] || {}; return colData.flex !== undefined ? colData.flex : 0; }, /** * Set the flex value for a grid row. * By default the row flex value is <code>0</code>. * * @param row {Integer} The row index * @param flex {Integer} The row's flex value * @return {qx.ui.layout.Grid} This object (for chaining support) */ setRowFlex(row, flex) { this._setRowData(row, "flex", flex); this._applyLayoutChange(); return this; }, /** * Get the flex value of a grid row. * * @param row {Integer} The row index * @return {Integer} The row's flex value */ getRowFlex(row) { var rowData = this.__rowData[row] || {}; var rowFlex = rowData.flex !== undefined ? rowData.flex : 0; return rowFlex; }, /** * Set the maximum width of a grid column. * The default value is <code>Infinity</code>. * * @param column {Integer} The column index * @param maxWidth {Integer} The column's maximum width * @return {qx.ui.layout.Grid} This object (for chaining support) */ setColumnMaxWidth(column, maxWidth) { this._setColumnData(column, "maxWidth", maxWidth); this._applyLayoutChange(); return this; }, /** * Get the maximum width of a grid column. * * @param column {Integer} The column index * @return {Integer} The column's maximum width */ getColumnMaxWidth(column) { var colData = this.__colData[column] || {}; return colData.maxWidth !== undefined ? colData.maxWidth : Infinity; }, /** * Set the preferred width of a grid column. * The default value is <code>Infinity</code>. * * @param column {Integer} The column index * @param width {Integer} The column's width * @return {qx.ui.layout.Grid} This object (for chaining support) */ setColumnWidth(column, width) { this._setColumnData(column, "width", width); this._applyLayoutChange(); return this; }, /** * Get the preferred width of a grid column. * * @param column {Integer} The column index * @return {Integer} The column's width */ getColumnWidth(column) { var colData = this.__colData[column] || {}; return colData.width !== undefined ? colData.width : null; }, /** * Set the minimum width of a grid column. * The default value is <code>0</code>. * * @param column {Integer} The column index * @param minWidth {Integer} The column's minimum width * @return {qx.ui.layout.Grid} This object (for chaining support) */ setColumnMinWidth(column, minWidth) { this._setColumnData(column, "minWidth", minWidth); this._applyLayoutChange(); return this; }, /** * Get the minimum width of a grid column. * * @param column {Integer} The column index * @return {Integer} The column's minimum width */ getColumnMinWidth(column) { var colData = this.__colData[column] || {}; return colData.minWidth || 0; }, /** * Set the maximum height of a grid row. * The default value is <code>Infinity</code>. * * @param row {Integer} The row index * @param maxHeight {Integer} The row's maximum width * @return {qx.ui.layout.Grid} This object (for chaining support) */ setRowMaxHeight(row, maxHeight) { this._setRowData(row, "maxHeight", maxHeight); this._applyLayoutChange(); return this; }, /** * Get the maximum height of a grid row. * * @param row {Integer} The row index * @return {Integer} The row's maximum width */ getRowMaxHeight(row) { var rowData = this.__rowData[row] || {}; return rowData.maxHeight || Infinity; }, /** * Set the preferred height of a grid row. * The default value is <code>Infinity</code>. * * @param row {Integer} The row index * @param height {Integer} The row's width * @return {qx.ui.layout.Grid} This object (for chaining support) */ setRowHeight(row, height) { this._setRowData(row, "height", height); this._applyLayoutChange(); return this; }, /** * Get the preferred height of a grid row. * * @param row {Integer} The row index * @return {Integer} The row's width */ getRowHeight(row) { var rowData = this.__rowData[row] || {}; return rowData.height !== undefined ? rowData.height : null; }, /** * Set the minimum height of a grid row. * The default value is <code>0</code>. * * @param row {Integer} The row index * @param minHeight {Integer} The row's minimum width * @return {qx.ui.layout.Grid} This object (for chaining support) */ setRowMinHeight(row, minHeight) { this._setRowData(row, "minHeight", minHeight); this._applyLayoutChange(); return this; }, /** * Get the minimum height of a grid row. * * @param row {Integer} The row index * @return {Integer} The row's minimum width */ getRowMinHeight(row) { var rowData = this.__rowData[row] || {}; return rowData.minHeight || 0; }, /** * Computes the widget's size hint including the widget's margins * * @param widget {qx.ui.core.LayoutItem} The widget to get the size for * @return {Map} a size hint map */ _getOuterSize(widget) { var hint = widget.getSizeHint(); var hMargins = widget.getMarginLeft() + widget.getMarginRight(); var vMargins = widget.getMarginTop() + widget.getMarginBottom(); var outerSize = { height: hint.height + vMargins, width: hint.width + hMargins, minHeight: hint.minHeight + vMargins, minWidth: hint.minWidth + hMargins, maxHeight: hint.maxHeight + vMargins, maxWidth: hint.maxWidth + hMargins }; return outerSize; }, /** * Check whether all row spans fit with their preferred height into the * preferred row heights. If there is not enough space, the preferred * row sizes are increased. The distribution respects the flex and max * values of the rows. * * The same is true for the min sizes. * * The height array is modified in place. * * @param rowHeights {Map[]} The current row height array as computed by * {@link #_getRowHeights}. */ _fixHeightsRowSpan(rowHeights) { var vSpacing = this.getSpacingY(); for (var i = 0, l = this.__rowSpans.length; i < l; i++) { var widget = this.__rowSpans[i]; var hint = this._getOuterSize(widget); var widgetProps = widget.getLayoutProperties(); var widgetRow = widgetProps.row; var prefSpanHeight = vSpacing * (widgetProps.rowSpan - 1); var minSpanHeight = prefSpanHeight; var rowFlexes = {}; for (var j = 0; j < widgetProps.rowSpan; j++) { var row = widgetProps.row + j; var rowHeight = rowHeights[row]; var rowFlex = this.getRowFlex(row); if (rowFlex > 0) { // compute flex array for the preferred height rowFlexes[row] = { min: rowHeight.minHeight, value: rowHeight.height, max: rowHeight.maxHeight, flex: rowFlex }; } prefSpanHeight += rowHeight.height; minSpanHeight += rowHeight.minHeight; } // If there is not enough space for the preferred size // increment the preferred row sizes. if (prefSpanHeight < hint.height) { if (!qx.lang.Object.isEmpty(rowFlexes)) { var rowIncrements = qx.ui.layout.Util.computeFlexOffsets( rowFlexes, hint.height, prefSpanHeight ); for (var k = 0; k < widgetProps.rowSpan; k++) { var offset = rowIncrements[widgetRow + k] ? rowIncrements[widgetRow + k].offset : 0; rowHeights[widgetRow + k].height += offset; } // row is too small and we have no flex value set } else { var totalSpacing = vSpacing * (widgetProps.rowSpan - 1); var availableHeight = hint.height - totalSpacing; // get the row height which every child would need to share the // available hight equally var avgRowHeight = Math.floor( availableHeight / widgetProps.rowSpan ); // get the hight already used and the number of children which do // not have at least that avg row height var usedHeight = 0; var rowsNeedAddition = 0; for (var k = 0; k < widgetProps.rowSpan; k++) { var currentHeight = rowHeights[widgetRow + k].height; usedHeight += currentHeight; if (currentHeight < avgRowHeight) { rowsNeedAddition++; } } // the difference of available and used needs to be shared among // those not having the min size var additionalRowHeight = Math.floor( (availableHeight - usedHeight) / rowsNeedAddition ); // add the extra height to the too small children for (var k = 0; k < widgetProps.rowSpan; k++) { if (rowHeights[widgetRow + k].height < avgRowHeight) { rowHeights[widgetRow + k].height += additionalRowHeight; } } } } // If there is not enough space for the min size // increment the min row sizes. if (minSpanHeight < hint.minHeight) { var rowIncrements = qx.ui.layout.Util.computeFlexOffsets( rowFlexes, hint.minHeight, minSpanHeight ); for (var j = 0; j < widgetProps.rowSpan; j++) { var offset = rowIncrements[widgetRow + j] ? rowIncrements[widgetRow + j].offset : 0; rowHeights[widgetRow + j].minHeight += offset; } } } }, /** * Check whether all col spans fit with their preferred width into the * preferred column widths. If there is not enough space the preferred * column sizes are increased. The distribution respects the flex and max * values of the columns. * * The same is true for the min sizes. * * The width array is modified in place. * * @param colWidths {Map[]} The current column width array as computed by * {@link #_getColWidths}. */ _fixWidthsColSpan(colWidths) { var hSpacing = this.getSpacingX(); var colSpans = this._getColSpans(); for (var i = 0, l = colSpans.length; i < l; i++) { var widget = colSpans[i]; var hint = this._getOuterSize(widget); var widgetProps = widget.getLayoutProperties(); var widgetColumn = widgetProps.column; var prefSpanWidth = hSpacing * (widgetProps.colSpan - 1); var minSpanWidth = prefSpanWidth; var colFlexes = {}; var offset; for (var j = 0; j < widgetProps.colSpan; j++) { var col = widgetProps.column + j; var colWidth = colWidths[col]; var colFlex = this.getColumnFlex(col); // compute flex array for the preferred width if (colFlex > 0) { colFlexes[col] = { min: colWidth.minWidth, value: colWidth.width, max: colWidth.maxWidth, flex: colFlex }; } prefSpanWidth += colWidth.width; minSpanWidth += colWidth.minWidth; } // If there is not enough space for the preferred size // increment the preferred column sizes. if (prefSpanWidth < hint.width) { // Do not adapt column widths to the width // of the spanning cell if allowGrowSpannedCellWidth property // is set to false // See https://github.com/qooxdoo/qooxdoo/issues/9871 if ( !this.getAllowGrowSpannedCellWidth() || !qx.lang.Object.isEmpty(colFlexes) ) { var colIncrements = qx.ui.layout.Util.computeFlexOffsets( colFlexes, hint.width, prefSpanWidth ); for (var j = 0; j < widgetProps.colSpan; j++) { offset = colIncrements[widgetColumn + j] ? colIncrements[widgetColumn + j].offset : 0; colWidths[widgetColumn + j].width += offset; } // col is too small and we have no flex value set } else { var totalSpacing = hSpacing * (widgetProps.colSpan - 1); var availableWidth = hint.width - totalSpacing; // get the col width which every child would need to share the // available width equally var avgColWidth = Math.floor(availableWidth / widgetProps.colSpan); // get the width already used and the number of children which do // not have at least that avg col width var usedWidth = 0; var colsNeedAddition = 0; for (var k = 0; k < widgetProps.colSpan; k++) { var currentWidth = colWidths[widgetColumn + k].width; usedWidth += currentWidth; if (currentWidth < avgColWidth) { colsNeedAddition++; } } // the difference of available and used needs to be shared among // those not having the min size var additionalColWidth = Math.floor( (availableWidth - usedWidth) / colsNeedAddition ); // add the extra width to the too small children for (var k = 0; k < widgetProps.colSpan; k++) { if (colWidths[widgetColumn + k].width < avgColWidth) { colWidths[widgetColumn + k].width += additionalColWidth; } } } } // If there is not enough space for the min size // increment the min column sizes. if (minSpanWidth < hint.minWidth) { var colIncrements = qx.ui.layout.Util.computeFlexOffsets( colFlexes, hint.minWidth, minSpanWidth ); for (var j = 0; j < widgetProps.colSpan; j++) { offset = colIncrements[widgetColumn + j] ? colIncrements[widgetColumn + j].offset : 0; colWidths[widgetColumn + j].minWidth += offset; } } } }, /** * Compute the min/pref/max row heights. * * @return {Map[]} An array containing height information for each row. The * entries have the keys <code>minHeight</code>, <code>maxHeight</code> and * <code>height</code>. */ _getRowHeights() { if (this.__rowHeights != null) { return this.__rowHeights; } var rowHeights = []; var maxRowIndex = this.__maxRowIndex; var maxColIndex = this.__maxColIndex; for (var row = 0; row <= maxRowIndex; row++) { var minHeight = 0; var height = 0; var maxHeight = 0; for (var col = 0; col <= maxColIndex; col++) { var widget = this.__grid[row][col]; if (!widget) { continue; } // ignore rows with row spans at this place // these rows will be taken into account later var widgetRowSpan = widget.getLayoutProperties().rowSpan || 0; if (widgetRowSpan > 1) { continue; } var cellSize = this._getOuterSize(widget); if (this.getRowFlex(row) > 0) { minHeight = Math.max(minHeight, cellSize.minHeight); } else { minHeight = Math.max(minHeight, cellSize.height); } height = Math.max(height, cellSize.height); } var minHeight = Math.max(minHeight, this.getRowMinHeight(row)); var maxHeight = this.getRowMaxHeight(row); if (this.getRowHeight(row) !== null) { var height = this.getRowHeight(row); } else { var height = Math.max(minHeight, Math.min(height, maxHeight)); } rowHeights[row] = { minHeight: minHeight, height: height, maxHeight: maxHeight }; } if (this.__rowSpans.length > 0) { this._fixHeightsRowSpan(rowHeights); } this.__rowHeights = rowHeights; return rowHeights; }, /** * Compute the min/pref/max column widths. * * @return {Map[]} An array containing width information for each column. The * entries have the keys <code>minWidth</code>, <code>maxWidth</code> and * <code>width</code>. */ _getColWidths() { if (this.__colWidths != null) { return this.__colWidths; } var colWidths = []; var maxColIndex = this.__maxColIndex; var maxRowIndex = this.__maxRowIndex; for (var col = 0; col <= maxColIndex; col++) { var width = 0; var minWidth = 0; var maxWidth = Infinity; for (var row = 0; row <= maxRowIndex; row++) { var widget = this.__grid[row][col]; if (!widget) { continue; } // ignore columns with col spans at this place // these columns will be taken into account later var widgetColSpan = widget.getLayoutProperties().colSpan || 0; if (widgetColSpan > 1) { continue; } var cellSize = this._getOuterSize(widget); minWidth = Math.max(minWidth, cellSize.minWidth); width = Math.max(width, cellSize.width); } minWidth = Math.max(minWidth, this.getColumnMinWidth(col)); maxWidth = this.getColumnMaxWidth(col); if (this.getColumnWidth(col) !== null) { var width = this.getColumnWidth(col); } else { var width = Math.max(minWidth, Math.min(width, maxWidth)); } colWidths[col] = { minWidth: minWidth, width: width, maxWidth: maxWidth }; } if (this._getColSpans().length > 0) { this._fixWidthsColSpan(colWidths); } this.__colWidths = colWidths; return colWidths; }, /** * Computes for each column by how many pixels it must grow or shrink, taking * the column flex values and min/max widths into account. * * @param width {Integer} The grid width * @return {Integer[]} Sparse array of offsets to add to each column width. If * an array entry is empty nothing should be added to the column. */ _getColumnFlexOffsets(width) { var hint = this.getSizeHint(); var diff = width - hint.width; if (diff == 0) { return {}; } // collect all flexible children var colWidths = this._getColWidths(); var flexibles = {}; for (var i = 0, l = colWidths.length; i < l; i++) { var col = colWidths[i]; var colFlex = this.getColumnFlex(i); if ( colFlex <= 0 || (col.width == col.maxWidth && diff > 0) || (col.width == col.minWidth && diff < 0) ) { continue; } flexibles[i] = { min: col.minWidth, value: col.width, max: col.maxWidth, flex: colFlex }; } return qx.ui.layout.Util.computeFlexOffsets(flexibles, width, hint.width); }, /** * Computes for each row by how many pixels it must grow or shrink, taking * the row flex values and min/max heights into account. * * @param height {Integer} The grid height * @return {Integer[]} Sparse array of offsets to add to each row height. If * an array entry is empty nothing should be added to the row. */ _getRowFlexOffsets(height) { var hint = this.getSizeHint(); var diff = height - hint.height; if (diff == 0) { return {}; } // collect all flexible children var rowHeights = this._getRowHeights(); var flexibles = {}; for (var i = 0, l = rowHeights.length; i < l; i++) { var row = rowHeights[i]; var rowFlex = this.getRowFlex(i); if ( rowFlex <= 0 || (row.height == row.maxHeight && diff > 0) || (row.height == row.minHeight && diff < 0) ) { continue; } flexibles[i] = { min: row.minHeight, value: row.height, max: row.maxHeight, flex: rowFlex }; } return qx.ui.layout.Util.computeFlexOffsets( flexibles, height, hint.height ); }, /** * Returns the internal private __colSpans array in order * have a protected getter which can be used other methods * to make them overridable * * @return {Array} the __colSpans array */ _getColSpans() { return this.__colSpans; }, // overridden renderLayout(availWidth, availHeight, padding) { if (this._invalidChildrenCache) { this.__buildGrid(); } var Util = qx.ui.layout.Util; var hSpacing = this.getSpacingX(); var vSpacing = this.getSpacingY(); // calculate column widths var prefWidths = this._getColWidths(); var colStretchOffsets = this._getColumnFlexOffsets(availWidth); var colWidths = []; var maxColIndex = this.__maxColIndex; var maxRowIndex = this.__maxRowIndex; var offset; for (var col = 0; col <= maxColIndex; col++) { offset = colStretchOffsets[col] ? colStretchOffsets[col].offset : 0; colWidths[col] = prefWidths[col].width + offset; } // calculate row heights var prefHeights = this._getRowHeights(); var rowStretchOffsets = this._getRowFlexOffsets(availHeight); var rowHeights = []; for (var row = 0; row <= maxRowIndex; row++) { offset = rowStretchOffsets[row] ? rowStretchOffsets[row].offset : 0; rowHeights[row] = prefHeights[row].height + offset; } // do the layout var left = 0; for (var col = 0; col <= maxColIndex; col++) { var top = 0; for (var row = 0; row <= maxRowIndex; row++) { var widget = this.__grid[row][col]; // ignore empty cells if (!widget) { top += rowHeights[row] + vSpacing; continue; } var widgetProps = widget.getLayoutProperties(); // ignore cells, which have cell spanning but are not the origin // of the widget if (widgetProps.row !== row || widgetProps.column !== col) { top += rowHeights[row] + vSpacing; continue; } // compute sizes width including cell spanning var spanWidth = hSpacing * (widgetProps.colSpan - 1); for (var i = 0; i < widgetProps.colSpan; i++) { spanWidth += colWidths[col + i]; } var spanHeight = vSpacing * (widgetProps.rowSpan - 1); for (var i = 0; i < widgetProps.rowSpan; i++) { spanHeight += rowHeights[row + i]; } var cellHint = widget.getSizeHint(); var marginTop = widget.getMarginTop(); var marginLeft = widget.getMarginLeft(); var marginBottom = widget.getMarginBottom(); var marginRight = widget.getMarginRight(); var cellWidth = Math.max( cellHint.minWidth, Math.min(spanWidth - marginLeft - marginRight, cellHint.maxWidth) ); var cellHeight = Math.max( cellHint.minHeight, Math.min(spanHeight - marginTop - marginBottom, cellHint.maxHeight) ); var cellAlign = this.getCellAlign(row, col); var cellLeft = left + Util.computeHorizontalAlignOffset( cellAlign.hAlign, cellWidth, spanWidth, marginLeft, marginRight ); var cellTop = top + Util.computeVerticalAlignOffset( cellAlign.vAlign, cellHeight, spanHeight, marginTop, marginBottom ); widget.renderLayout( cellLeft + padding.left, cellTop + padding.top, cellWidth, cellHeight ); top += rowHeights[row] + vSpacing; } left += colWidths[col] + hSpacing; } }, // overridden invalidateLayoutCache() { super.invalidateLayoutCache(); this.__colWidths = null; this.__rowHeights = null; }, // overridden _computeSizeHint() { if (this._invalidChildrenCache) { this.__buildGrid(); } // calculate col widths var colWidths = this._getColWidths(); var minWidth = 0, width = 0; for (var i = 0, l = colWidths.length; i < l; i++) { var col = colWidths[i]; if (this.getColumnFlex(i) > 0) { minWidth += col.minWidth; } else { minWidth += col.width; } width += col.width; } // calculate row heights var rowHeights = this._getRowHeights(); var minHeight = 0, height = 0; for (var i = 0, l = rowHeights.length; i < l; i++) { var row = rowHeights[i]; if (this.getRowFlex(i) > 0) { minHeight += row.minHeight; } else { minHeight += row.height; } height += row.height; } var spacingX = this.getSpacingX() * (colWidths.length - 1); var spacingY = this.getSpacingY() * (rowHeights.length - 1); var hint = { minWidth: minWidth + spacingX, width: width + spacingX, minHeight: minHeight + spacingY, height: height + spacingY }; return hint; } }, /* ***************************************************************************** DESTRUCT ***************************************************************************** */ destruct() { this.__grid = this.__rowData = this.__colData = this.__colSpans = this.__rowSpans = this.__colWidths = this.__rowHeights = null; } });