/* * Copyright 2000-2022 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.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; 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.Registration; import com.vaadin.shared.ui.MarginInfo; import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc; import com.vaadin.shared.ui.gridlayout.GridLayoutState; import com.vaadin.shared.ui.gridlayout.GridLayoutState.ChildComponentData; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; /** * A layout where the components are laid out on a grid using cell coordinates. * *
* The GridLayout also maintains a cursor for adding components in * left-to-right, top-to-bottom order. *
* *
* Each component in a GridLayout
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.
*
* The grid coordinates, which are specified by a row and column index, always * start from 0 for the topmost row and the leftmost column. *
* * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings("serial") public class GridLayout extends AbstractLayout implements Layout.AlignmentHandler, Layout.SpacingHandler, Layout.MarginHandler, LayoutClickNotifier { private GridLayoutServerRpc rpc = (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; private final LinkedList* 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. *
* ** 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. *
* * @param component * the component to be added, notnull
.
* @param column1
* the column of the upper left corner of the area c
* is supposed to occupy. The leftmost column has index 0.
* @param row1
* the row of the upper left corner of the area c
is
* supposed to occupy. The topmost row has index 0.
* @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 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(String.format(
"Illegal coordinates for the component: %s!<=%s, %s!<=%s",
column1, column2, row1, row2));
}
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 Maparea
overlaps with any existing area.
*/
private void checkExistingOverlaps(Area area) throws OverlapsException {
for (Entrynull
.
* @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.
*
* * Sets the cursor column to 0 and increments the cursor row by one. *
* ** By calling this function you can ensure that no more components are added * right of the previous component. *
* * @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, notnull
.
*/
@Override
public void addComponent(Component component) {
if (component == null) {
throw new IllegalArgumentException("Component must not be null");
}
// 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;
}
getState().childData.remove(component);
components.remove(component);
super.removeComponent(component);
}
/**
* 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 Component component : components) {
final ChildComponentData childData = getState().childData
.get(component);
if (childData.column1 == column && childData.row1 == row) {
removeComponent(component);
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* Also maintains a reference to the component contained in the area. *
* ** 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. *
* * @author Vaadin Ltd. * @since 3.0 */ public class Area implements Serializable { private final ChildComponentData childData; private final 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. 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.component = component; childData = new ChildComponentData(); childData.alignment = getDefaultComponentAlignment().getBitMask(); childData.column1 = column1; childData.row1 = row1; childData.column2 = column2; childData.row2 = row2; } public Area(ChildComponentData childData, Component component) { this.childData = childData; 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 * @returntrue
if other
area overlaps with
* this on, false
if it does not.
*/
public boolean overlaps(Area other) {
return componentsOverlap(childData, other.childData);
}
/**
* Gets the component connected to the area.
*
* @return the Component.
*/
public Component getComponent() {
return component;
}
/**
* Gets the column of the top-left corner cell.
*
* @return the column of the top-left corner cell.
*/
public int getColumn1() {
return childData.column1;
}
/**
* Gets the column of the bottom-right corner cell.
*
* @return the column of the bottom-right corner cell.
*/
public int getColumn2() {
return childData.column2;
}
/**
* Gets the row of the top-left corner cell.
*
* @return the row of the top-left corner cell.
*/
public int getRow1() {
return childData.row1;
}
/**
* Gets the row of the bottom-right corner cell.
*
* @return the row of the bottom-right corner cell.
*/
public int getRow2() {
return childData.row2;
}
@Override
public String toString() {
return String.format("Area{%s,%s - %s,%s}", getColumn1(), getRow1(),
getColumn2(), getRow2());
}
}
private static boolean componentsOverlap(ChildComponentData a,
ChildComponentData b) {
return a.column1 <= b.column2 && a.row1 <= b.row2
&& a.column2 >= b.column1 && a.row2 >= b.row1;
}
/**
* Gridlayout does not support laying components on top of each other. An
* OverlapsException
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 RuntimeException {
private final Area existingArea;
/**
* Constructs an OverlapsException
.
*
* @param existingArea
* the existing area that needs overlapping
*/
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.childData.column1);
sb.append(',');
sb.append(existingArea.childData.column1);
sb.append(',');
sb.append(existingArea.childData.row1);
sb.append(',');
sb.append(existingArea.childData.row2);
sb.append("(column1, column2, row1, row2).");
return sb.toString();
}
/**
* 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 Vaadin Ltd.
* @since 3.0
*/
public class OutOfBoundsException extends RuntimeException {
private final Area areaOutOfBounds;
/**
* Constructs an OoutOfBoundsException
with the specified
* detail message.
*
* @param areaOutOfBounds
* the area that exceeds the bounds of the grid
*/
public OutOfBoundsException(Area areaOutOfBounds) {
super(String.format("%s, layout dimension: %sx%s", areaOutOfBounds,
getColumns(), getRows()));
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 (Entry* 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, starting from 0. */ public int getCursorX() { return cursorX; } /** * Sets the current cursor x-position. This is usually handled automatically * by GridLayout. * * @param cursorX * current cursor x-position */ public void setCursorX(int cursorX) { this.cursorX = cursorX; } /** * Gets the current y-position (row) of the cursor. * ** 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. *
* * @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 ChildComponentData oldLocation = getState().childData.get(oldComponent); ChildComponentData newLocation = getState().childData.get(newComponent); if (oldLocation == null) { addComponent(newComponent); } else if (newLocation == null) { removeComponent(oldComponent); addComponent(newComponent, oldLocation.column1, oldLocation.row1, oldLocation.column2, oldLocation.row2); } else { int oldAlignment = oldLocation.alignment; oldLocation.alignment = newLocation.alignment; newLocation.alignment = oldAlignment; getState().childData.put(newComponent, oldLocation); getState().childData.put(oldComponent, newLocation); } } /* * Removes all components from this container. * * @see com.vaadin.ui.ComponentContainer#removeAllComponents() */ @Override public void removeAllComponents() { super.removeAllComponents(); cursorX = 0; cursorY = 0; } @Override public void setComponentAlignment(Component childComponent, Alignment alignment) { ChildComponentData childComponentData = getState().childData .get(childComponent); if (childComponentData == null) { throw new IllegalArgumentException( "Component must be added to layout before using setComponentAlignment()"); } else { if (alignment == null) { childComponentData.alignment = GridLayoutState.ALIGNMENT_DEFAULT .getBitMask(); } else { childComponentData.alignment = alignment.getBitMask(); } } } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) */ @Override public void setSpacing(boolean spacing) { getState().spacing = spacing; } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() */ @Override public boolean isSpacing() { return getState(false).spacing; } /** * 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 (ChildComponentData existingArea : getState().childData.values()) { // 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); markAsDirty(); } /** * Removes a row and all the components in the row. * ** Components which span over several rows are removed if the selected row * is on the first row of such a component. *
* ** 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. *
* * @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 (ChildComponentData existingArea : getState().childData.values()) { 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--; } } markAsDirty(); } /** * Sets the expand ratio of given column. * ** 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. *
* ** Note, that width of this GridLayout needs to be defined (fixed or * relative, as opposed to undefined height) for this method to have any * effect. *
* Note that checking for relative width for the child components is done on
* the server so you cannot set a child component to have undefined width on
* the server and set it to 100%
in CSS. You must set it to
* 100%
on the server.
*
* @see #setWidth(float, Unit)
*
* @param columnIndex
* The column index, starting from 0 for the leftmost row.
* @param ratio
* the expand ratio
*/
public void setColumnExpandRatio(int columnIndex, float ratio) {
columnExpandRatio.put(columnIndex, ratio);
getState().explicitColRatios.add(columnIndex);
markAsDirty();
}
/**
* Returns the expand ratio of given column.
*
* @see #setColumnExpandRatio(int, float)
*
* @param columnIndex
* The column index, starting from 0 for the leftmost row.
* @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.
*
*
* 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. *
* ** Note, that height of this GridLayout needs to be defined (fixed or * relative, as opposed to undefined height) for this method to have any * effect. *
* Note that checking for relative height for the child components is done
* on the server so you cannot set a child component to have undefined
* height on the server and set it to
* After reading the design, cursorY is set to point to a row outside of the
* GridLayout area. CursorX is reset to 0.
*/
@Override
public void readDesign(Element design, DesignContext designContext) {
super.readDesign(design, designContext);
setMargin(readMargin(design, getMargin(), designContext));
if (design.childNodeSize() > 0) {
// Touch content only if there is some content specified. This is
// needed to be able to use extended GridLayouts which add
// components in the constructor (e.g. Designs based on GridLayout).
readChildComponents(design.children(), designContext);
}
// Set cursor position explicitly
setCursorY(getRows());
setCursorX(0);
}
private void readChildComponents(Elements childElements,
DesignContext designContext) {
List100%
in CSS. You must set
* it to 100%
on the server.
*
* @see #setHeight(float, Unit)
*
* @param rowIndex
* The row index, starting from 0 for the topmost row.
* @param ratio
* the expand ratio
*/
public void setRowExpandRatio(int rowIndex, float ratio) {
rowExpandRatio.put(rowIndex, ratio);
getState().explicitRowRatios.add(rowIndex);
markAsDirty();
}
/**
* 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 (Entry