diff options
Diffstat (limited to 'server/src/com/vaadin/ui/GridLayout.java')
-rw-r--r-- | server/src/com/vaadin/ui/GridLayout.java | 1419 |
1 files changed, 1419 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/GridLayout.java b/server/src/com/vaadin/ui/GridLayout.java new file mode 100644 index 0000000000..5a748c04f6 --- /dev/null +++ b/server/src/com/vaadin/ui/GridLayout.java @@ -0,0 +1,1419 @@ +/* + * Copyright 2011 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Map.Entry; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.EventId; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc; +import com.vaadin.shared.ui.gridlayout.GridLayoutState; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * A layout where the components are laid out on a grid using cell coordinates. + * + * <p> + * The GridLayout also maintains a cursor for adding components in + * left-to-right, top-to-bottom order. + * </p> + * + * <p> + * Each component in a <code>GridLayout</code> uses a defined + * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The + * components may not overlap with the existing components - if you try to do so + * you will get an {@link OverlapsException}. Adding a component with cursor + * automatically extends the grid by increasing the grid height. + * </p> + * + * <p> + * The grid coordinates, which are specified by a row and column index, always + * start from 0 for the topmost row and the leftmost column. + * </p> + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings("serial") +public class GridLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier, + Vaadin6Component { + + private GridLayoutServerRpc rpc = new GridLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(GridLayout.this, + mouseDetails, clickedConnector)); + + } + }; + /** + * Cursor X position: this is where the next component with unspecified x,y + * is inserted + */ + private int cursorX = 0; + + /** + * Cursor Y position: this is where the next component with unspecified x,y + * is inserted + */ + private int cursorY = 0; + + /** + * Contains all items that are placed on the grid. These are components with + * grid area definition. + */ + private final LinkedList<Area> areas = new LinkedList<Area>(); + + /** + * Mapping from components to their respective areas. + */ + private final LinkedList<Component> components = new LinkedList<Component>(); + + /** + * Mapping from components to alignments (horizontal + vertical). + */ + private Map<Component, Alignment> componentToAlignment = new HashMap<Component, Alignment>(); + + private static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT; + + /** + * Has there been rows inserted or deleted in the middle of the layout since + * the last paint operation. + */ + private boolean structuralChange = false; + + private Map<Integer, Float> columnExpandRatio = new HashMap<Integer, Float>(); + private Map<Integer, Float> rowExpandRatio = new HashMap<Integer, Float>(); + + /** + * Constructor for a grid of given size (number of columns and rows). + * + * The grid may grow or shrink later. Grid grows automatically if you add + * components outside its area. + * + * @param columns + * Number of columns in the grid. + * @param rows + * Number of rows in the grid. + */ + public GridLayout(int columns, int rows) { + setColumns(columns); + setRows(rows); + registerRpc(rpc); + } + + /** + * Constructs an empty (1x1) grid layout that is extended as needed. + */ + public GridLayout() { + this(1, 1); + } + + @Override + public GridLayoutState getState() { + return (GridLayoutState) super.getState(); + } + + /** + * <p> + * Adds a component to the grid in the specified area. The area is defined + * by specifying the upper left corner (column1, row1) and the lower right + * corner (column2, row2) of the area. The coordinates are zero-based. + * </p> + * + * <p> + * If the area overlaps with any of the existing components already present + * in the grid, the operation will fail and an {@link OverlapsException} is + * thrown. + * </p> + * + * @param component + * the component to be added. + * @param column1 + * the column of the upper left corner of the area <code>c</code> + * is supposed to occupy. The leftmost column has index 0. + * @param row1 + * the row of the upper left corner of the area <code>c</code> is + * supposed to occupy. The topmost row has index 0. + * @param column2 + * the column of the lower right corner of the area + * <code>c</code> is supposed to occupy. + * @param row2 + * the row of the lower right corner of the area <code>c</code> + * 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 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 + final 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 >= getColumns() + || row2 >= getRows()) { + 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); + final Iterator<Area> i = areas.iterator(); + int index = 0; + boolean done = false; + while (!done && i.hasNext()) { + final Area existingArea = 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); + } + + // Attempt to add to super + try { + super.addComponent(component); + } catch (IllegalArgumentException e) { + areas.remove(area); + components.remove(component); + throw e; + } + + // update cursor position, if it's within this area; use first position + // outside this area, even if it's occupied + if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1 + && cursorY <= row2) { + // cursor within area + cursorX = column2 + 1; // one right of area + if (cursorX >= getColumns()) { + // overflowed columns + cursorX = 0; // first col + // move one row down, or one row under the area + cursorY = (column1 == 0 ? row2 : row1) + 1; + } else { + cursorY = row1; + } + } + + 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 <code>area</code> overlaps with any existing area. + */ + private void checkExistingOverlaps(Area area) throws OverlapsException { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area existingArea = i.next(); + if (existingArea.overlaps(area)) { + // Component not added, overlaps with existing component + throw new OverlapsException(existingArea); + } + } + } + + /** + * Adds the component to the grid in cells column1,row1 (NortWest corner of + * the area.) End coordinates (SouthEast corner of the area) are the same as + * column1,row1. The coordinates are zero-based. Component width and height + * is 1. + * + * @param component + * the component to be added. + * @param column + * the column index, starting from 0. + * @param row + * the row index, starting from 0. + * @throws OverlapsException + * if the new component overlaps with any of the components + * already in the grid. + * @throws OutOfBoundsException + * if the cell is outside the grid area. + */ + public void addComponent(Component component, int column, int row) + throws OverlapsException, OutOfBoundsException { + this.addComponent(component, column, row, column, row); + } + + /** + * Forces the next component to be added at the beginning of the next line. + * + * <p> + * Sets the cursor column to 0 and increments the cursor row by one. + * </p> + * + * <p> + * By calling this function you can ensure that no more components are added + * right of the previous component. + * </p> + * + * @see #space() + */ + public void newLine() { + cursorX = 0; + cursorY++; + } + + /** + * Moves the cursor forward by one. If the cursor goes out of the right grid + * border, it is moved to the first column of the next row. + * + * @see #newLine() + */ + public void space() { + cursorX++; + if (cursorX >= getColumns()) { + 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 component + * the component to be added. + */ + @Override + 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 (final OverlapsException e) { + space(); + } + } + + // Extends the grid if needed + if (cursorX >= getColumns()) { + setColumns(cursorX + 1); + } + if (cursorY >= getRows()) { + setRows(cursorY + 1); + } + + addComponent(component, cursorX, cursorY); + } + + /** + * Removes the specified component from the layout. + * + * @param component + * the component to be removed. + */ + @Override + public void removeComponent(Component component) { + + // Check that the component is contained in the container + if (component == null || !components.contains(component)) { + return; + } + + Area area = null; + for (final Iterator<Area> i = areas.iterator(); area == null + && i.hasNext();) { + final Area a = i.next(); + if (a.getComponent() == component) { + area = a; + } + } + + components.remove(component); + if (area != null) { + areas.remove(area); + } + + componentToAlignment.remove(component); + + super.removeComponent(component); + + requestRepaint(); + } + + /** + * Removes the component specified by its cell coordinates. + * + * @param column + * the component's column, starting from 0. + * @param row + * the component's row, starting from 0. + */ + public void removeComponent(int column, int row) { + + // Finds the area + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.getColumn1() == column && area.getRow1() == row) { + removeComponent(area.getComponent()); + return; + } + } + } + + /** + * Gets an Iterator for the components contained in the layout. By using the + * Iterator it is possible to step through the contents of the layout. + * + * @return the Iterator of the components inside the layout. + */ + @Override + public Iterator<Component> getComponentIterator() { + return Collections.unmodifiableCollection(components).iterator(); + } + + /** + * Gets the number of components contained in the layout. Consistent with + * the iterator returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + + /** + * Paints the contents of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + // TODO refactor attribute names in future release. + target.addAttribute("structuralChange", structuralChange); + structuralChange = false; + + // Area iterator + final Iterator<Area> areaiterator = areas.iterator(); + + // Current item to be processed (fetch first item) + Area area = areaiterator.hasNext() ? (Area) areaiterator.next() : null; + + // Collects rowspan related information here + final HashMap<Integer, Integer> cellUsed = new HashMap<Integer, Integer>(); + + // Empty cell collector + int emptyCells = 0; + + final String[] alignmentsArray = new String[components.size()]; + final Integer[] columnExpandRatioArray = new Integer[getColumns()]; + final Integer[] rowExpandRatioArray = new Integer[getRows()]; + + int realColExpandRatioSum = 0; + float colSum = getExpandRatioSum(columnExpandRatio); + if (colSum == 0) { + // no columns has been expanded, all cols have same expand + // rate + float equalSize = 1 / (float) getColumns(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < getColumns(); i++) { + columnExpandRatioArray[i] = myRatio; + } + realColExpandRatioSum = myRatio * getColumns(); + } else { + for (int i = 0; i < getColumns(); i++) { + int myRatio = Math + .round((getColumnExpandRatio(i) / colSum) * 1000); + columnExpandRatioArray[i] = myRatio; + realColExpandRatioSum += myRatio; + } + } + + boolean equallyDividedRows = false; + int realRowExpandRatioSum = 0; + float rowSum = getExpandRatioSum(rowExpandRatio); + if (rowSum == 0) { + // no rows have been expanded + equallyDividedRows = true; + float equalSize = 1 / (float) getRows(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < getRows(); i++) { + rowExpandRatioArray[i] = myRatio; + } + realRowExpandRatioSum = myRatio * getRows(); + } + + int index = 0; + + // Iterates every applicable row + for (int cury = 0; cury < getRows(); cury++) { + target.startTag("gr"); + + if (!equallyDividedRows) { + int myRatio = Math + .round((getRowExpandRatio(cury) / rowSum) * 1000); + rowExpandRatioArray[cury] = myRatio; + realRowExpandRatioSum += myRatio; + + } + // Iterates every applicable column + for (int curx = 0; curx < getColumns(); 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 + final int cols = (area.column2 - area.column1) + 1; + final 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); + } + LegacyPaint.paint(area.getComponent(), target); + + alignmentsArray[index++] = String + .valueOf(getComponentAlignment(area.getComponent()) + .getBitMask()); + + target.endTag("gc"); + + // Fetch next item + if (areaiterator.hasNext()) { + 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 + final int rowspanDepth = 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(Integer.valueOf(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", getColumns() - 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 + + // correct possible rounding error + if (rowExpandRatioArray.length > 0) { + rowExpandRatioArray[0] -= realRowExpandRatioSum - 1000; + } + if (columnExpandRatioArray.length > 0) { + columnExpandRatioArray[0] -= realColExpandRatioSum - 1000; + } + + target.addAttribute("colExpand", columnExpandRatioArray); + target.addAttribute("rowExpand", rowExpandRatioArray); + + // Add child component alignment info to layout tag + target.addAttribute("alignments", alignmentsArray); + + } + + private float getExpandRatioSum(Map<Integer, Float> ratioMap) { + float sum = 0; + for (Iterator<Entry<Integer, Float>> iterator = ratioMap.entrySet() + .iterator(); iterator.hasNext();) { + sum += iterator.next().getValue(); + } + return sum; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com + * .vaadin.ui.Component) + */ + @Override + public Alignment getComponentAlignment(Component childComponent) { + Alignment alignment = componentToAlignment.get(childComponent); + if (alignment == null) { + return ALIGNMENT_DEFAULT; + } else { + return alignment; + } + } + + /** + * Defines a rectangular area of cells in a GridLayout. + * + * <p> + * Also maintains a reference to the component contained in the area. + * </p> + * + * <p> + * The area is specified by the cell coordinates of its upper left corner + * (column1,row1) and lower right corner (column2,row2). As otherwise with + * GridLayout, the column and row coordinates start from zero. + * </p> + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public class Area implements Serializable { + + /** + * The column of the upper left corner cell of the area. + */ + private final 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 final int column2; + + /** + * The row of the lower right corner cell of the area. + */ + private int row2; + + /** + * Component painted in the area. + */ + private Component component; + + /** + * <p> + * Construct a new area on a grid. + * </p> + * + * @param component + * the component connected to the area. + * @param column1 + * The column of the upper left corner cell of the area. The + * leftmost column has index 0. + * @param row1 + * The row of the upper left corner cell of the area. The + * topmost row has index 0. + * @param column2 + * The column of the lower right corner cell of the area. The + * leftmost column has index 0. + * @param row2 + * The row of the lower right corner cell of the area. The + * topmost row has index 0. + */ + 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 this Area overlaps with another Area. + * + * @param other + * the other Area that is to be tested for overlap with this + * area + * @return <code>true</code> if <code>other</code> area overlaps with + * this on, <code>false</code> if it does not. + */ + 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. + * + * <p> + * This function only sets the value in the data structure and does not + * send any events or set parents. + * </p> + * + * @param newComponent + * the new connected overriding the existing one. + */ + protected void setComponent(Component newComponent) { + component = newComponent; + } + + /** + * @deprecated Use {@link #getColumn1()} instead. + */ + @Deprecated + 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 {@link #getColumn2()} instead. + */ + @Deprecated + 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 {@link #getRow1()} instead. + */ + @Deprecated + 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 {@link #getRow2()} instead. + */ + @Deprecated + 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; + } + + } + + /** + * Gridlayout does not support laying components on top of each other. An + * <code>OverlapsException</code> is thrown when a component already exists + * (even partly) at the same space on a grid with the new component. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public class OverlapsException extends java.lang.RuntimeException { + + private final Area existingArea; + + /** + * Constructs an <code>OverlapsException</code>. + * + * @param existingArea + */ + public OverlapsException(Area existingArea) { + this.existingArea = existingArea; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(); + Component component = existingArea.getComponent(); + sb.append(component); + sb.append("( type = "); + sb.append(component.getClass().getName()); + if (component.getCaption() != null) { + sb.append(", caption = \""); + sb.append(component.getCaption()); + sb.append("\""); + } + sb.append(")"); + sb.append(" is already added to "); + sb.append(existingArea.column1); + sb.append(","); + sb.append(existingArea.column1); + sb.append(","); + sb.append(existingArea.row1); + sb.append(","); + sb.append(existingArea.row2); + sb.append("(column1, column2, row1, row2)."); + + return sb.toString(); + } + + /** + * Gets the area . + * + * @return the existing area. + */ + public Area getArea() { + return existingArea; + } + } + + /** + * An <code>Exception</code> object which is thrown when an area exceeds the + * bounds of the grid. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public class OutOfBoundsException extends java.lang.RuntimeException { + + private final Area areaOutOfBounds; + + /** + * Constructs an <code>OoutOfBoundsException</code> 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 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 (getColumns() == columns) { + return; + } + + // Checks for overlaps + if (getColumns() > columns) { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.column2 >= columns) { + throw new OutOfBoundsException(area); + } + } + } + + getState().setColumns(columns); + + requestRepaint(); + } + + /** + * Get the number of columns in the grid. + * + * @return the number of columns in the grid. + */ + public int getColumns() { + return getState().getColumns(); + } + + /** + * 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 (getRows() == rows) { + return; + } + + // Checks for overlaps + if (getRows() > rows) { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.row2 >= rows) { + throw new OutOfBoundsException(area); + } + } + } + + getState().setRows(rows); + + requestRepaint(); + } + + /** + * Get the number of rows in the grid. + * + * @return the number of rows in the grid. + */ + public int getRows() { + return getState().getRows(); + } + + /** + * Gets the current x-position (column) of the cursor. + * + * <p> + * 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. + * </p> + * + * @return the grid column the cursor is on, starting from 0. + */ + public int getCursorX() { + return cursorX; + } + + /** + * Sets the current cursor x-position. This is usually handled automatically + * by GridLayout. + * + * @param cursorX + */ + public void setCursorX(int cursorX) { + this.cursorX = cursorX; + } + + /** + * Gets the current y-position (row) of the cursor. + * + * <p> + * 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 the first free + * position after the cursor. + * </p> + * + * @return the grid row the Cursor is on. + */ + public int getCursorY() { + return cursorY; + } + + /** + * Sets the current y-coordinate (row) of the cursor. This is usually + * handled automatically by GridLayout. + * + * @param cursorY + * the row number, starting from 0 for the topmost row. + */ + public void setCursorY(int cursorY) { + this.cursorY = cursorY; + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + Area oldLocation = null; + Area newLocation = null; + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area location = i.next(); + final 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.vaadin.ui.ComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + super.removeAllComponents(); + componentToAlignment = new HashMap<Component, Alignment>(); + cursorX = 0; + cursorY = 0; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .vaadin.ui.Component, int, int) + */ + @Override + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + componentToAlignment.put(childComponent, new Alignment( + horizontalAlignment + verticalAlignment)); + requestRepaint(); + } + + @Override + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + componentToAlignment.put(childComponent, alignment); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + @Override + public void setSpacing(boolean spacing) { + getState().setSpacing(spacing); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Override + public boolean isSpacing() { + return getState().isSpacing(); + } + + /** + * Inserts an empty row at the specified position in the grid. + * + * @param row + * Index of the row before which the new row will be inserted. + * The leftmost row has index 0. + */ + public void insertRow(int row) { + if (row > getRows()) { + throw new IllegalArgumentException("Cannot insert row at " + row + + " in a gridlayout with height " + getRows()); + } + + for (Iterator<Area> i = areas.iterator(); i.hasNext();) { + Area existingArea = i.next(); + // Areas ending below the row needs to be moved down or stretched + if (existingArea.row2 >= row) { + existingArea.row2++; + + // Stretch areas that span over the selected row + if (existingArea.row1 >= row) { + existingArea.row1++; + } + + } + } + + if (cursorY >= row) { + cursorY++; + } + + setRows(getRows() + 1); + structuralChange = true; + requestRepaint(); + } + + /** + * Removes a row and all the components in the row. + * + * <p> + * Components which span over several rows are removed if the selected row + * is on the first row of such a component. + * </p> + * + * <p> + * If the last row is removed then all remaining components will be removed + * and the grid will be reduced to one row. The cursor will be moved to the + * upper left cell of the grid. + * </p> + * + * @param row + * Index of the row to remove. The leftmost row has index 0. + */ + public void removeRow(int row) { + if (row >= getRows()) { + throw new IllegalArgumentException("Cannot delete row " + row + + " from a gridlayout with height " + getRows()); + } + + // Remove all components in row + for (int col = 0; col < getColumns(); col++) { + removeComponent(col, row); + } + + // Shrink or remove areas in the selected row + for (Iterator<Area> i = areas.iterator(); i.hasNext();) { + Area existingArea = i.next(); + if (existingArea.row2 >= row) { + existingArea.row2--; + + if (existingArea.row1 > row) { + existingArea.row1--; + } + } + } + + if (getRows() == 1) { + /* + * Removing the last row means that the dimensions of the Grid + * layout will be truncated to 1 empty row and the cursor is moved + * to the first cell + */ + cursorX = 0; + cursorY = 0; + } else { + setRows(getRows() - 1); + if (cursorY > row) { + cursorY--; + } + } + + structuralChange = true; + requestRepaint(); + + } + + /** + * Sets the expand ratio of given column. + * + * <p> + * The expand ratio defines how excess space is distributed among columns. + * Excess space means space that is left over from components that are not + * sized relatively. By default, the excess space is distributed evenly. + * </p> + * + * <p> + * Note that the component width of the GridLayout must be defined (fixed or + * relative, as opposed to undefined) for this method to have any effect. + * </p> + * + * @see #setWidth(float, int) + * + * @param columnIndex + * @param ratio + */ + public void setColumnExpandRatio(int columnIndex, float ratio) { + columnExpandRatio.put(columnIndex, ratio); + requestRepaint(); + } + + /** + * Returns the expand ratio of given column + * + * @see #setColumnExpandRatio(int, float) + * + * @param columnIndex + * @return the expand ratio, 0.0f by default + */ + public float getColumnExpandRatio(int columnIndex) { + Float r = columnExpandRatio.get(columnIndex); + return r == null ? 0 : r.floatValue(); + } + + /** + * Sets the expand ratio of given row. + * + * <p> + * Expand ratio defines how excess space is distributed among rows. Excess + * space means the space left over from components that are not sized + * relatively. By default, the excess space is distributed evenly. + * </p> + * + * <p> + * Note, that height needs to be defined (fixed or relative, as opposed to + * undefined height) for this method to have any effect. + * </p> + * + * @see #setHeight(float, int) + * + * @param rowIndex + * The row index, starting from 0 for the topmost row. + * @param ratio + */ + public void setRowExpandRatio(int rowIndex, float ratio) { + rowExpandRatio.put(rowIndex, ratio); + requestRepaint(); + } + + /** + * Returns the expand ratio of given row. + * + * @see #setRowExpandRatio(int, float) + * + * @param rowIndex + * The row index, starting from 0 for the topmost row. + * @return the expand ratio, 0.0f by default + */ + public float getRowExpandRatio(int rowIndex) { + Float r = rowExpandRatio.get(rowIndex); + return r == null ? 0 : r.floatValue(); + } + + /** + * Gets the Component at given index. + * + * @param x + * The column index, starting from 0 for the leftmost column. + * @param y + * The row index, starting from 0 for the topmost row. + * @return Component in given cell or null if empty + */ + public Component getComponent(int x, int y) { + for (final Iterator<Area> iterator = areas.iterator(); iterator + .hasNext();) { + final Area area = iterator.next(); + if (area.getColumn1() <= x && x <= area.getColumn2() + && area.getRow1() <= y && y <= area.getRow2()) { + return area.getComponent(); + } + } + return null; + } + + /** + * Returns information about the area where given component is laid in the + * GridLayout. + * + * @param component + * the component whose area information is requested. + * @return an Area object that contains information how component is laid in + * the grid + */ + public Area getComponentArea(Component component) { + for (final Iterator<Area> iterator = areas.iterator(); iterator + .hasNext();) { + final Area area = iterator.next(); + if (area.getComponent() == component) { + return area; + } + } + return null; + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + +} |