/*
* Copyright 2008 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package com.sun.lwuit.table;
import com.sun.lwuit.layouts.*;
import com.sun.lwuit.Component;
import com.sun.lwuit.Container;
import com.sun.lwuit.Display;
import com.sun.lwuit.geom.Dimension;
import com.sun.lwuit.plaf.Style;
/**
* Layout manager similar in spirit to HTML tables allowing rows and columns
* of varying width/height.
*
* @author Shai Almog
*/
public class TableLayout extends Layout {
private int currentRow;
private int currentColumn;
private static int minimumSizePerColumn = 10;
private Constraint[][] tablePositions;
private int[] columnSizes;
private int[] columnPositions;
private int[] rowPositions;
private boolean[] modifableColumnSize;
/**
* Special case marker SPAN constraint reserving place for other elements
*/
private static final Constraint SPAN_CONSTRAINT = new Constraint();
private static int defaultColumnWidth = -1;
private static int defaultRowHeight = -1;
private boolean horizontalSpanningExists;
private boolean verticalSpanningExists;
/**
* A table must declare the amount of rows and columns in advance
*
* @param rows rows of the table
* @param columns columns of the table
*/
public TableLayout(int rows, int columns) {
tablePositions = new Constraint[rows][columns];
}
/**
* Returns the component at the given row/column
*
* @param row the row of the component
* @param column the column of the component
* @return the component instance
*/
public Component getComponentAt(int row, int column) {
return tablePositions[row][column].parent;
}
/**
* @inheritDoc
*/
public void layoutContainer(Container parent) {
verticalSpanningExists = false;
horizontalSpanningExists = false;
// column and row size in pixels
Style s = parent.getStyle();
int top = s.getPadding(false, Component.TOP);
int left = s.getPadding(parent.isRTL(), Component.LEFT);
int bottom = s.getPadding(false, Component.BOTTOM);
int right = s.getPadding(parent.isRTL(), Component.RIGHT);
boolean rtl = parent.isRTL();
columnSizes = new int[tablePositions[0].length];
if(modifableColumnSize == null) {
modifableColumnSize = new boolean[tablePositions[0].length];
}
columnPositions = new int[tablePositions[0].length];
int[] rowSizes = new int[tablePositions.length];
rowPositions = new int[tablePositions.length];
int pWidth = parent.getLayoutWidth() - parent.getSideGap() - left - right;
int pHeight = parent.getLayoutHeight() - parent.getBottomGap() - top - bottom;
int currentX = left;
for(int iter = 0 ; iter < columnSizes.length ; iter++) {
columnSizes[iter] = getColumnWidthPixels(iter, pWidth, pWidth);
}
// try to recalculate the columns for none horizontally scrollable tables
// so they are distributed sensibly if no room is available
if(!parent.isScrollableX()) {
int totalWidth = 0;
int totalModifyablePixels = 0;
// check how many columns we can modify (the user hasn't requested a specific size for those)
for(int iter = 0 ; iter < modifableColumnSize.length ; iter++) {
if(modifableColumnSize[iter]) {
totalModifyablePixels += columnSizes[iter];
}
totalWidth += columnSizes[iter];
}
if(pWidth < totalWidth) {
int totalPixelsToRemove = totalWidth - pWidth;
int totalPixelsNecessary = totalModifyablePixels - totalPixelsToRemove;
// Go over the modifyable columns and remove the right pixels according to the ratio
for(int iter = 0 ; iter < modifableColumnSize.length ; iter++) {
if(modifableColumnSize[iter]) {
columnSizes[iter] = (int)(((float)columnSizes[iter]) / ((float)totalModifyablePixels) * totalPixelsNecessary);
}
}
}
}
for(int iter = 0 ; iter < columnSizes.length ; iter++) {
if(rtl) {
currentX += columnSizes[iter];
columnPositions[iter] = pWidth - currentX;
} else {
columnPositions[iter] = currentX;
currentX += columnSizes[iter];
}
}
int currentY = top;
for(int iter = 0 ; iter < rowSizes.length ; iter++) {
if(parent.isScrollableY()) {
rowSizes[iter] = getRowHeightPixels(iter, pHeight, -1);
} else {
rowSizes[iter] = getRowHeightPixels(iter, pHeight, pHeight - currentY);
}
rowPositions[iter] = currentY;
currentY += rowSizes[iter];
}
for(int r = 0 ; r < rowSizes.length ; r++) {
for(int c = 0 ; c < columnSizes.length ; c++) {
Constraint con = tablePositions[r][c];
int conX, conY, conW, conH;
if(con != null && con != SPAN_CONSTRAINT) {
Style componentStyle = con.parent.getStyle();
int leftMargin = componentStyle.getMargin(parent.isRTL(), Component.LEFT);
int topMargin = componentStyle.getMargin(false, Component.TOP);
conX = left + leftMargin + columnPositions[c];
conY = top + topMargin + rowPositions[r];
if(con.spanHorizontal > 1) {
horizontalSpanningExists = true;
int w = columnSizes[c];
for(int sh = 1 ; sh < con.spanHorizontal ; sh++) {
w += columnSizes[c + sh];
}
// for RTL we need to move the component to the side so spanning will work
if(rtl) {
conX = left + leftMargin + columnPositions[c + con.spanHorizontal - 1];
}
conW = w - leftMargin - componentStyle.getMargin(parent.isRTL(), Component.RIGHT);
} else {
conW = columnSizes[c] - leftMargin - componentStyle.getMargin(parent.isRTL(), Component.RIGHT);
}
if(con.spanVertical > 1) {
verticalSpanningExists = true;
int h = rowSizes[r];
for(int sv = 1 ; sv < con.spanVertical ; sv++) {
h += rowSizes[r + sv];
}
conH = h - topMargin - componentStyle.getMargin(false, Component.BOTTOM);
} else {
conH = rowSizes[r] - topMargin - componentStyle.getMargin(false, Component.BOTTOM);
}
placeComponent(rtl, con, conX, conY, conW, conH);
}
}
}
}
/**
* Returns the position of the given table row. A valid value is only returned after the
* layout occurred.
*
* @param row the row in the table
* @return the Y position in pixels or -1 if layout hasn't occured/row is too large etc.
*/
public int getRowPosition(int row) {
if(rowPositions != null && rowPositions.length > row) {
return rowPositions[row];
}
return -1;
}
/**
* Returns the position of the given table column. A valid value is only returned after the
* layout occurred.
*
* @param col the column in the table
* @return the X position in pixels or -1 if layout hasn't occured/column is too large etc.
*/
public int getColumnPosition(int col) {
if(columnPositions != null && columnPositions.length > col) {
return columnPositions[col];
}
return -1;
}
/**
* Places the component/constraint in the proper alignment within the cell whose bounds are given
*/
private void placeComponent(boolean rtl, Constraint con, int x, int y, int width, int height) {
con.parent.setX(x);
con.parent.setY(y);
con.parent.setWidth(width);
con.parent.setHeight(height);
Dimension pref = con.parent.getPreferredSize();
int pWidth = pref.getWidth();
int pHeight = pref.getHeight();
if(pWidth < width) {
int d = (width - pWidth);
int a = con.align;
if(rtl) {
switch(a) {
case Component.LEFT:
a = Component.RIGHT;
break;
case Component.RIGHT:
a = Component.LEFT;
break;
}
}
switch(a) {
case Component.LEFT:
con.parent.setX(x);
con.parent.setWidth(width - d);
break;
case Component.RIGHT:
con.parent.setX(x + d);
con.parent.setWidth(width - d);
break;
case Component.CENTER:
con.parent.setX(x + d / 2);
con.parent.setWidth(width - d);
break;
}
}
if(pHeight < height) {
int d = (height - pHeight);
switch(con.valign) {
case Component.TOP:
con.parent.setY(y);
con.parent.setHeight(height - d);
break;
case Component.BOTTOM:
con.parent.setY(y + d);
con.parent.setHeight(height - d);
break;
case Component.CENTER:
con.parent.setY(y + d / 2);
con.parent.setHeight(height - d);
break;
}
}
}
private int getColumnWidthPixels(int column, int percentageOf, int available) {
int current = 0;
if(modifableColumnSize == null) {
modifableColumnSize = new boolean[tablePositions[0].length];
}
for(int iter = 0 ; iter < tablePositions.length ; iter++) {
Constraint c = tablePositions[iter][column];
if(c == null || c == SPAN_CONSTRAINT || c.spanHorizontal > 1) {
continue;
}
// width in percentage of the parent container
if(c.width > 0 && available > -1) {
current = Math.max(current, c.width * percentageOf / 100);
modifableColumnSize[column] = false;
} else {
Style s = c.parent.getStyle();
current = Math.max(current, c.parent.getPreferredW() + s.getMargin(false, Component.LEFT) + s.getMargin(false, Component.RIGHT));
modifableColumnSize[column] = true;
}
if(available > -1) {
current = Math.min(available, current);
}
}
return current;
}
private int getRowHeightPixels(int row, int percentageOf, int available) {
int current = 0;
for(int iter = 0 ; iter < tablePositions[row].length ; iter++) {
Constraint c = tablePositions[row][iter];
if(c == null || c == SPAN_CONSTRAINT || c.spanVertical > 1) {
continue;
}
// height in percentage of the parent container
if(c.height > 0) {
current = Math.max(current, c.height * percentageOf / 100);
} else {
Style s = c.parent.getStyle();
current = Math.max(current, c.parent.getPreferredH() + s.getMargin(false, Component.BOTTOM) + s.getMargin(false, Component.TOP));
}
if(available > -1) {
current = Math.min(available, current);
}
}
return current;
}
/**
* @inheritDoc
*/
public Dimension getPreferredSize(Container parent) {
Style s = parent.getStyle();
int w = s.getPadding(false, Component.LEFT) + s.getPadding(false, Component.RIGHT);
int h = s.getPadding(false, Component.TOP) + s.getPadding(false, Component.BOTTOM);
for(int iter = 0 ; iter < tablePositions[0].length ; iter++) {
w += getColumnWidthPixels(iter, Integer.MAX_VALUE, -1);
}
for(int iter = 0 ; iter < tablePositions.length ; iter++) {
h += getRowHeightPixels(iter, Integer.MAX_VALUE, -1);
}
return new Dimension(w, h);
}
/**
* Returns the row where the next operation of add will appear
*
* @return the row where the next operation of add will appear
*/
public int getNextRow() {
return currentRow;
}
/**
* Returns the column where the next operation of add will appear
*
* @return the column where the next operation of add will appear
*/
public int getNextColumn() {
return currentColumn;
}
/**
* @inheritDoc
*/
public void addLayoutComponent(Object value, Component comp, Container c) {
Constraint con = (Constraint)value;
if(con == null) {
con = createConstraint();
} else {
if(con.parent != null) {
throw new IllegalArgumentException("Constraint already associated with component!");
}
}
if(con.row < 0) {
con.row = currentRow;
}
if(con.column < 0) {
con.column = currentColumn;
}
con.parent = comp;
if(tablePositions[con.row][con.column] != null) {
throw new IllegalArgumentException("Row: " + con.row + " and column: " + con.column + " already occupied");
}
tablePositions[con.row][con.column] = con;
if(con.spanHorizontal > 1 || con.spanVertical > 1) {
for(int sh = 0 ; sh < con.spanHorizontal ; sh++) {
for(int sv = 0 ; sv < con.spanVertical ; sv++) {
if(sh > 0 || sv > 0) {
if(tablePositions[con.row + sv][con.column + sh] == null) {
tablePositions[con.row + sv][con.column + sh] = SPAN_CONSTRAINT;
}
}
}
}
}
updateRowColumn();
}
private void updateRowColumn() {
if(currentRow >= tablePositions.length) {
return;
}
while(tablePositions[currentRow][currentColumn] != null) {
currentColumn++;
if(currentColumn >= tablePositions[0].length) {
currentColumn = 0;
currentRow++;
if(currentRow >= tablePositions.length) {
return;
}
}
}
}
/**
* Returns the spanning for the table cell at the given coordinate
*
* @param row row in the table
* @param column column within the table
* @return the amount of spanning 1 for no spanning
*/
public int getCellHorizontalSpan(int row, int column) {
return tablePositions[row][column].spanHorizontal;
}
/**
* Returns the spanning for the table cell at the given coordinate
*
* @param row row in the table
* @param column column within the table
* @return the amount of spanning 1 for no spanning
*/
public int getCellVerticalSpan(int row, int column) {
return tablePositions[row][column].spanVertical;
}
/**
* Indicates whether there is spanning within this layout
*
* @return true if the layout makes use of spanning
*/
public boolean hasVerticalSpanning() {
return verticalSpanningExists;
}
/**
* Indicates whether there is spanning within this layout
*
* @return true if the layout makes use of spanning
*/
public boolean hasHorizontalSpanning() {
return horizontalSpanningExists;
}
/**
* @inheritDoc
*/
public void removeLayoutComponent(Component comp) {
for(int r = 0 ; r < tablePositions.length ; r++) {
for(int c = 0 ; c < tablePositions[r].length ; c++) {
if(tablePositions[r][c] != null && tablePositions[r][c].parent == comp) {
tablePositions[r][c] = null;
return;
}
}
}
}
/**
* @inheritDoc
*/
public Object getComponentConstraint(Component comp) {
for(int r = 0 ; r < tablePositions.length ; r++) {
for(int c = 0 ; c < tablePositions[r].length ; c++) {
if(tablePositions[r][c] != null && tablePositions[r][c].parent == comp) {
return tablePositions[r][c];
}
}
}
return null;
}
/**
* Creates a new Constraint instance to add to the layout
*
* @return the default constraint
*/
public Constraint createConstraint() {
return new Constraint();
}
/**
* Creates a new Constraint instance to add to the layout
*
* @param row the row for the table starting with 0
* @param column the column for the table starting with 0
* @return the new constraint
*/
public Constraint createConstraint(int row, int column) {
Constraint c = createConstraint();
c.row = row;
c.column = column;
return c;
}
/**
* Sets the minimum size for a column in the table, this is applicable for tables that are
* not scrollable on the X axis. This will force the earlier columns to leave room for
* the latter columns.
*
* @param minimumSize the minimum width of the column
*/
public static void setMinimumSizePerColumn(int minimumSize) {
minimumSizePerColumn = minimumSize;
}
/**
* Indicates the minimum size for a column in the table, this is applicable for tables that are
* not scrollable on the X axis. This will force the earlier columns to leave room for
* the latter columns.
*
* @return the minimum width of the column
*/
public static int getMinimumSizePerColumn() {
return minimumSizePerColumn;
}
/**
* Indicates the default (in percentage) for the column width, -1 indicates
* automatic sizing
*
* @param w width in percentage
*/
public static void setDefaultColumnWidth(int w) {
defaultColumnWidth = w;
}
/**
* Indicates the default (in percentage) for the column width, -1 indicates
* automatic sizing
*
* @return width in percentage
*/
public static int getDefaultColumnWidth() {
return defaultColumnWidth;
}
/**
* Indicates the default (in percentage) for the row height, -1 indicates
* automatic sizing
*
* @param h height in percentage
*/
public static void setDefaultRowHeight(int h) {
defaultRowHeight = h;
}
/**
* Indicates the default (in percentage) for the row height, -1 indicates
* automatic sizing
*
* @return height in percentage
*/
public static int getDefaultRowHeight() {
return defaultRowHeight;
}
/**
* Represents the layout constraint for an entry within the table indicating
* the desired position/behavior of the component.
*/
public static class Constraint {
private Component parent;
private int row = -1;
private int column = -1;
private int width = defaultColumnWidth;
private int height = defaultRowHeight;
private int spanHorizontal = 1;
private int spanVertical = 1;
private int align = -1;
private int valign = -1;
/**
* Sets the cells to span vertically, this number must never be smaller than 1
*
* @param span a number larger than 1
*/
public void setVerticalSpan(int span) {
if(span < 1) {
throw new IllegalArgumentException("Illegal span");
}
spanVertical = span;
}
/**
* Sets the cells to span horizontally, this number must never be smaller than 1
*
* @param span a number larger than 1
*/
public void setHorizontalSpan(int span) {
if(span < 1) {
throw new IllegalArgumentException("Illegal span");
}
spanHorizontal = span;
}
/**
* Sets the column width based on percentage of the parent
*
* @param width negative number indicates ignoring this member
*/
public void setWidthPercentage(int width) {
this.width = width;
}
/**
* Sets the row height based on percentage of the parent
*
* @param height negative number indicates ignoring this member
*/
public void setHeightPercentage(int height) {
this.height = height;
}
/**
* Sets the horizontal alignment of the table cell
*
* @param align Component.LEFT/RIGHT/CENTER
*/
public void setHorizontalAlign(int align) {
this.align = align;
}
/**
* Sets the vertical alignment of the table cell
*
* @param valign Component.TOP/BOTTOM/CENTER
*/
public void setVerticalAlign(int valign) {
this.valign = valign;
}
}
}