/* ************************************************************************* IT Mill Toolkit Development of Browser User Interfaces Made Easy Copyright (C) 2000-2007 IT Mill Ltd ************************************************************************* This product is distributed under commercial license that can be found from the product package on license.pdf. Use of this product might require purchasing a commercial license from IT Mill Ltd. For guidelines on usage, see licensing-guidelines.html ************************************************************************* For more information, contact: IT Mill Ltd phone: +358 2 4802 7180 Ruukinkatu 2-4 fax: +358 2 4802 7181 20540, Turku email: info@itmill.com Finland company www: www.itmill.com Primary source for information and releases: www.itmill.com ********************************************************************** */ package com.itmill.toolkit.ui; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import com.itmill.toolkit.terminal.PaintException; import com.itmill.toolkit.terminal.PaintTarget; import com.itmill.toolkit.terminal.Sizeable; import com.itmill.toolkit.terminal.gwt.client.ui.AlignmentInfo; /** *
* A container that consists of components with certain coordinates (cell * position) on a grid. It also maintains cursor for adding component in left to * right, top to bottom order. *
* *
* Each component in a GridLayout
uses a certain
* {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. One
* should not add components that would overlap with the existing components
* because in such case an {@link OverlapsException} is thrown. Adding component
* with cursor automatically extends the grid by increasing the grid height.
*
* Adds a component with a specified area to the grid. The area the new * component should take is defined by specifying the upper left corner * (column1, row1) and the lower right corner (column2, row2) of the area. *
* ** If the new component overlaps with any of the existing components already * present in the grid the operation will fail and an * {@link OverlapsException} is thrown. *
* * @param c * the component to be added. * @param column1 * the column of the upper left corner of the area *c
is supposed to occupy.
* @param row1
* the row of the upper left corner of the area
* c
is supposed to occupy.
* @param column2
* the column of the lower right corner of the area
* c
is supposed to occupy.
* @param row2
* the row of the lower right corner of the area
* c
is supposed to occupy.
* @throws OverlapsException
* if the new component overlaps with any of the components
* already in the grid.
* @throws OutOfBoundsException
* if the cells are outside of the grid area.
*/
public void addComponent(Component component, int column1, int row1,
int column2, int row2) throws OverlapsException,
OutOfBoundsException {
if (component == null) {
throw new NullPointerException("Component must not be null");
}
// Checks that the component does not already exist in the container
if (components.contains(component)) {
throw new IllegalArgumentException(
"Component is already in the container");
}
// Creates the area
Area area = new Area(component, column1, row1, column2, row2);
// Checks the validity of the coordinates
if (column2 < column1 || row2 < row1) {
throw new IllegalArgumentException(
"Illegal coordinates for the component");
}
if (column1 < 0 || row1 < 0 || column2 >= cols || row2 >= rows) {
throw new OutOfBoundsException(area);
}
// Checks that newItem does not overlap with existing items
checkExistingOverlaps(area);
// Inserts the component to right place at the list
// Respect top-down, left-right ordering
component.setParent(this);
Iterator i = areas.iterator();
int index = 0;
boolean done = false;
while (!done && i.hasNext()) {
Area existingArea = (Area) i.next();
if ((existingArea.row1 >= row1 && existingArea.column1 > column1)
|| existingArea.row1 > row1) {
areas.add(index, area);
components.add(index, component);
done = true;
}
index++;
}
if (!done) {
areas.addLast(area);
components.addLast(component);
}
super.addComponent(component);
requestRepaint();
}
/**
* Tests if the given area overlaps with any of the items already on the
* grid.
*
* @param area
* the Area to be checked for overlapping.
* @throws OverlapsException
* if area
overlaps with any existing area.
*/
private void checkExistingOverlaps(Area area) throws OverlapsException {
for (Iterator i = areas.iterator(); i.hasNext();) {
Area existingArea = (Area) i.next();
if (existingArea.overlaps(area)) {
// Component not added, overlaps with existing component
throw new OverlapsException(existingArea);
}
}
}
/**
* Adds the component into this container to cells column1,row1 (NortWest
* corner of the area.) End coordinates (SouthEast corner of the area) are
* the same as column1,row1. Component width and height is 1.
*
* @param c
* the component to be added.
* @param column
* the column index.
* @param row
* the row index.
*/
public void addComponent(Component c, int column, int row) {
this.addComponent(c, column, row, column, row);
}
/**
* Force the next component to be added to the beginning of the next line.
* By calling this function user can ensure that no more components are
* added to the right of the previous component.
*
* @see #space()
*/
public void newLine() {
cursorX = 0;
cursorY++;
}
/**
* Moves the cursor forwards by one. If the cursor goes out of the right
* grid border, move it to next line.
*
* @see #newLine()
*/
public void space() {
cursorX++;
if (cursorX >= cols) {
cursorX = 0;
cursorY++;
}
}
/**
* Adds the component into this container to the cursor position. If the
* cursor position is already occupied, the cursor is moved forwards to find
* free position. If the cursor goes out from the bottom of the grid, the
* grid is automatically extended.
*
* @param c
* the component to be added.
*/
public void addComponent(Component component) {
// Finds first available place from the grid
Area area;
boolean done = false;
while (!done) {
try {
area = new Area(component, cursorX, cursorY, cursorX, cursorY);
checkExistingOverlaps(area);
done = true;
} catch (OverlapsException ignored) {
space();
}
}
// Extends the grid if needed
cols = cursorX >= cols ? cursorX + 1 : cols;
rows = cursorY >= rows ? cursorY + 1 : rows;
addComponent(component, cursorX, cursorY);
}
/**
* Removes the given component from this container.
*
* @param c
* the component to be removed.
*/
public void removeComponent(Component component) {
// Check that the component is contained in the container
if (component == null || !components.contains(component)) {
return;
}
super.removeComponent(component);
Area area = null;
for (Iterator i = areas.iterator(); area == null && i.hasNext();) {
Area a = (Area) i.next();
if (a.getComponent() == component) {
area = a;
}
}
components.remove(component);
if (area != null) {
areas.remove(area);
}
componentToAlignment.remove(component);
requestRepaint();
}
/**
* Removes the component specified with it's cell index.
*
* @param column
* the Component's column.
* @param row
* the Component's row.
*/
public void removeComponent(int column, int row) {
// Finds the area
for (Iterator i = areas.iterator(); i.hasNext();) {
Area area = (Area) i.next();
if (area.getColumn1() == column && area.getRow1() == row) {
removeComponent(area.getComponent());
return;
}
}
}
/**
* Gets an Iterator to the component container contents. Using the Iterator
* it's possible to step through the contents of the container.
*
* @return the Iterator of the components inside the container.
*/
public Iterator getComponentIterator() {
return Collections.unmodifiableCollection(components).iterator();
}
/**
* Paints the contents of this component.
*
* @param target
* the Paint Event.
* @throws PaintException
* if the paint operation failed.
*/
public void paintContent(PaintTarget target) throws PaintException {
super.paintContent(target);
// TODO refactor attribute names in future release.
target.addAttribute("h", rows);
target.addAttribute("w", cols);
if (spacing) {
target.addAttribute("spacing", spacing);
}
// Area iterator
Iterator areaiterator = areas.iterator();
// Current item to be processed (fetch first item)
Area area = areaiterator.hasNext() ? (Area) areaiterator.next() : null;
// Collects rowspan related information here
HashMap cellUsed = new HashMap();
// Empty cell collector
int emptyCells = 0;
// Iterates every applicable row
for (int cury = 0; cury < rows; cury++) {
target.startTag("gr");
// Iterates every applicable column
for (int curx = 0; curx < cols; curx++) {
// Checks if current item is located at curx,cury
if (area != null && (area.row1 == cury)
&& (area.column1 == curx)) {
// First check if empty cell needs to be rendered
if (emptyCells > 0) {
target.startTag("gc");
target.addAttribute("x", curx - emptyCells);
target.addAttribute("y", cury);
if (emptyCells > 1) {
target.addAttribute("w", emptyCells);
}
target.endTag("gc");
emptyCells = 0;
}
// Now proceed rendering current item
int cols = (area.column2 - area.column1) + 1;
int rows = (area.row2 - area.row1) + 1;
target.startTag("gc");
target.addAttribute("x", curx);
target.addAttribute("y", cury);
if (cols > 1) {
target.addAttribute("w", cols);
}
if (rows > 1) {
target.addAttribute("h", rows);
}
area.getComponent().paint(target);
target.endTag("gc");
// Fetch next item
if (areaiterator.hasNext()) {
area = (Area) areaiterator.next();
} else {
area = null;
}
// Updates the cellUsed if rowspan needed
if (rows > 1) {
int spannedx = curx;
for (int j = 1; j <= cols; j++) {
cellUsed.put(new Integer(spannedx), new Integer(
cury + rows - 1));
spannedx++;
}
}
// Skips the current item's spanned columns
if (cols > 1) {
curx += cols - 1;
}
} else {
// Checks against cellUsed, render space or ignore cell
if (cellUsed.containsKey(new Integer(curx))) {
// Current column contains already an item,
// check if rowspan affects at current x,y position
int rowspanDepth = ((Integer) cellUsed.get(new Integer(
curx))).intValue();
if (rowspanDepth >= cury) {
// ignore cell
// Check if empty cell needs to be rendered
if (emptyCells > 0) {
target.startTag("gc");
target.addAttribute("x", curx - emptyCells);
target.addAttribute("y", cury);
if (emptyCells > 1) {
target.addAttribute("w", emptyCells);
}
target.endTag("gc");
emptyCells = 0;
}
} else {
// empty cell is needed
emptyCells++;
// Removes the cellUsed key as it has become
// obsolete
cellUsed.remove(new Integer(curx));
}
} else {
// empty cell is needed
emptyCells++;
}
}
} // iterates every column
// Last column handled of current row
// Checks if empty cell needs to be rendered
if (emptyCells > 0) {
target.startTag("gc");
target.addAttribute("x", cols - emptyCells);
target.addAttribute("y", cury);
if (emptyCells > 1) {
target.addAttribute("w", emptyCells);
}
target.endTag("gc");
emptyCells = 0;
}
target.endTag("gr");
} // iterates every row
// Last row handled
}
/**
* Gets the components UIDL tag.
*
* @return the Component UIDL tag as string.
* @see com.itmill.toolkit.ui.AbstractComponent#getTag()
*/
public String getTag() {
return "gridlayout";
}
/**
* This class defines an area on a grid. An Area is defined by the cells of
* its upper left corner (column1,row1) and lower right corner
* (column2,row2).
*
* @author IT Mill Ltd.
* @version
* @VERSION@
* @since 3.0
*/
public class Area {
/**
* The column of the upper left corner cell of the area.
*/
private int column1;
/**
* The row of the upper left corner cell of the area.
*/
private int row1;
/**
* The column of the lower right corner cell of the area.
*/
private int column2;
/**
* The row of the lower right corner cell of the area.
*/
private int row2;
/**
* Component painted on the area.
*/
private Component component;
/**
* * Construct a new area on a grid. *
* * @param component * the component connected to the area. * @param column1 * The column of the upper left corner cell of the area *c
is supposed to occupy.
* @param row1
* The row of the upper left corner cell of the area
* c
is supposed to occupy.
* @param column2
* The column of the lower right corner cell of the area
* c
is supposed to occupy.
* @param row2
* The row of the lower right corner cell of the area
* c
is supposed to occupy.
* @throws OverlapsException
* if the new component overlaps with any of the
* components already in the grid
*/
public Area(Component component, int column1, int row1, int column2,
int row2) {
this.column1 = column1;
this.row1 = row1;
this.column2 = column2;
this.row2 = row2;
this.component = component;
}
/**
* Tests if the given Area overlaps with an another Area.
*
* @param other
* the Another Area that's to be tested for overlap with
* this area.
* @return true
if other
overlaps with
* this area, false
if it doesn't.
*/
public boolean overlaps(Area other) {
return column1 <= other.getColumn2() && row1 <= other.getRow2()
&& column2 >= other.getColumn1() && row2 >= other.getRow1();
}
/**
* Gets the component connected to the area.
*
* @return the Component.
*/
public Component getComponent() {
return component;
}
/**
* Sets the component connected to the area.
*
* * This function only sets the value in the datastructure and does not * send any events or set parents. *
* * @param newComponent * the new connected overriding the existing one. */ protected void setComponent(Component newComponent) { component = newComponent; } /** * @deprecated Use getColumn1() instead. * * @see com.itmill.toolkit.ui.GridLayout#getColumn1() */ public int getX1() { return getColumn1(); } /** * Gets the column of the top-left corner cell. * * @return the column of the top-left corner cell. */ public int getColumn1() { return column1; } /** * @deprecated Use getColumn2() instead. * * @see com.itmill.toolkit.ui.GridLayout#getColumn2() */ public int getX2() { return getColumn2(); } /** * Gets the column of the bottom-right corner cell. * * @return the column of the bottom-right corner cell. */ public int getColumn2() { return column2; } /** * @deprecated Use getRow1() instead. * * @see com.itmill.toolkit.ui.GridLayout#getRow1() */ public int getY1() { return getRow1(); } /** * Gets the row of the top-left corner cell. * * @return the row of the top-left corner cell. */ public int getRow1() { return row1; } /** * @deprecated Use getRow2() instead. * * @see com.itmill.toolkit.ui.GridLayout#getRow2() */ public int getY2() { return getRow2(); } /** * Gets the row of the bottom-right corner cell. * * @return the row of the bottom-right corner cell. */ public int getRow2() { return row2; } } /** * AnException
object which is thrown when two Items occupy
* the same space on a grid.
*
* @author IT Mill Ltd.
* @version
* @VERSION@
* @since 3.0
*/
public class OverlapsException extends java.lang.RuntimeException {
/**
* Serial generated by eclipse.
*/
private static final long serialVersionUID = 3978144339870101561L;
private Area existingArea;
/**
* Constructs an OverlapsException
.
*
* @param existingArea
*/
public OverlapsException(Area existingArea) {
this.existingArea = existingArea;
}
/**
* Gets the area .
*
* @return the existing area.
*/
public Area getArea() {
return existingArea;
}
}
/**
* An Exception
object which is thrown when an area exceeds
* the bounds of the grid.
*
* @author IT Mill Ltd.
* @version
* @VERSION@
* @since 3.0
*/
public class OutOfBoundsException extends java.lang.RuntimeException {
/**
* Serial generated by eclipse.
*/
private static final long serialVersionUID = 3618985589664592694L;
private Area areaOutOfBounds;
/**
* Constructs an OoutOfBoundsException
with the specified
* detail message.
*
* @param areaOutOfBounds
*/
public OutOfBoundsException(Area areaOutOfBounds) {
this.areaOutOfBounds = areaOutOfBounds;
}
/**
* Gets the area that is out of bounds.
*
* @return the area out of Bound.
*/
public Area getArea() {
return areaOutOfBounds;
}
}
/**
* Sets the width of the layout.
* * NOTE: The behaviour of this methdod has changed in * version 5.0. Now this method won't set the number of columns in the grid * like it used to (use {@link #setColumns()} for that). Instead, it sets * the actual visual width of the layout in pixels or in another unit * specified in {@link Sizeable}.UNIT_SYMBOLS. *
*/ public void setWidth(int width) { super.setWidth(width); } /** * Gets the width of the layout. ** NOTE: The behaviour of this methdod has changed in * version 5.0. Now this method won't return the number of columns in the * grid like it used to (use {@link #getColumns()} for that). Instead, it * returns the actual visual width of the layout in pixels or in another * unit specified in {@link Sizeable}.UNIT_SYMBOLS. *
*/ public int getWidth() { return super.getWidth(); } /** * Sets the number of columns in the grid. The column count can not be * reduced if there are any areas that would be outside of the shrunk grid. * * @param columns * the new number of columns in the grid. */ public void setColumns(int columns) { // The the param if (columns < 1) { throw new IllegalArgumentException( "The number of columns and rows in the grid must be at least 1"); } // In case of no change if (cols == columns) { return; } // Checks for overlaps if (cols > columns) { for (Iterator i = areas.iterator(); i.hasNext();) { Area area = (Area) i.next(); if (area.column2 >= columns) { throw new OutOfBoundsException(area); } } } cols = columns; requestRepaint(); } /** * Get the number of columns in the grid. * * @return the number of columns in the grid. */ public final int getColumns() { return cols; } /** * Set the height of the layout. ** NOTE: The behaviour of this methdod has changed in * version 5.0. Now this method won't set the number of rows in the grid * like it used to (use {@link #setRows()} for that). Instead, it sets the * actual visual height of the layout in pixels or in another unit specified * in {@link Sizeable}.UNIT_SYMBOLS. *
*/ public void setHeight(int height) { super.setHeight(height); } /** * Gets the height of the layout. ** NOTE: The behaviour of this methdod has changed in * version 5.0. Now this method won't return the number of rows in the grid * like it used to (use {@link #getRows()} for that). Instead, it returns * the actual visual height of the layout in pixels or in another unit * specified in {@link Sizeable}.UNIT_SYMBOLS. *
*/ public int getHeight() { return super.getHeight(); } /** * Sets the number of rows in the grid. The number of rows can not be * reduced if there are any areas that would be outside of the shrunk grid. * * @param rows * the new number of rows in the grid. */ public void setRows(int rows) { // The the param if (rows < 1) { throw new IllegalArgumentException( "The number of columns and rows in the grid must be at least 1"); } // In case of no change if (this.rows == rows) { return; } // Checks for overlaps if (this.rows > rows) { for (Iterator i = areas.iterator(); i.hasNext();) { Area area = (Area) i.next(); if (area.row2 >= rows) { throw new OutOfBoundsException(area); } } } this.rows = rows; requestRepaint(); } /** * Get the number of rows in the grid. * * @return the number of rows in the grid. */ public final int getRows() { return rows; } /** * Gets the current cursor x-position. The cursor position points the * position for the next component that is added without specifying its * coordinates (grid cell). When the cursor position is occupied, the next * component will be added to first free position after the cursor. * * @return the grid column the Cursor is on. */ public int getCursorX() { return cursorX; } /** * Gets the current cursor y-position. The cursor position points the * position for the next component that is added without specifying its * coordinates (grid cell). When the cursor position is occupied, the next * component will be added to first free position after the cursor. * * @return the grid row the Cursor is on. */ public int getCursorY() { return cursorY; } /* Documented in superclass */ public void replaceComponent(Component oldComponent, Component newComponent) { // Gets the locations Area oldLocation = null; Area newLocation = null; for (Iterator i = areas.iterator(); i.hasNext();) { Area location = (Area) i.next(); Component component = location.getComponent(); if (component == oldComponent) { oldLocation = location; } if (component == newComponent) { newLocation = location; } } if (oldLocation == null) { addComponent(newComponent); } else if (newLocation == null) { removeComponent(oldComponent); addComponent(newComponent, oldLocation.getColumn1(), oldLocation .getRow1(), oldLocation.getColumn2(), oldLocation.getRow2()); } else { oldLocation.setComponent(newComponent); newLocation.setComponent(oldComponent); requestRepaint(); } } /* * Removes all components from this container. * * @see com.itmill.toolkit.ui.ComponentContainer#removeAllComponents() */ public void removeAllComponents() { super.removeAllComponents(); componentToAlignment = new HashMap(); cursorX = 0; cursorY = 0; } /** * Set alignment for one contained component in this layout. * * @param childComponent * the component to align within it's layout cell. * @param horizontalAlignment * the horizontal alignment for the child component (left, * center, right). * @param verticalAlignment * the vertical alignment for the child component (top, * center, bottom). */ public void setComponentAlignment(Component childComponent, int horizontalAlignment, int verticalAlignment) { componentToAlignment.put(childComponent, new Integer( horizontalAlignment + verticalAlignment)); } /** * Enable spacing between child components within this layout. * ** NOTE: This will only affect spaces between components, * not also all around spacing of the layout (i.e. do not mix this with HTML * Table elements cellspacing-attribute). Use {@link #setMargin(boolean)} to * add extra space around the layout. *
* * @param enabled */ public void setSpacing(boolean enabled) { spacing = enabled; } }